1
0
forked from MeloNX/MeloNX

Compare commits

...

10 Commits

64 changed files with 2995 additions and 5790 deletions

131
README.md
View File

@ -12,108 +12,118 @@
<p align="center"> <p align="center">
MeloNX is an iOS Nintendo Switch emulator based on Ryujinx, written primarily in C#. Designed to bring accurate performance and a user-friendly interface to iOS, MeloNX makes Switch games accessible on Apple devices. MeloNX is an iOS Nintendo Switch emulator based on Ryujinx, written primarily in C#. Designed to bring accurate performance and a user-friendly interface to iOS, MeloNX makes Switch games accessible on Apple devices.
Developed from the ground up, MeloNX is open-source and available on Github under the <a href="https://github.com/MeloNX-Emu/MeloNX/blob/master/LICENSE.txt" target="_blank">MeloNX license (Based on MIT)</a>. <br Developed from the ground up, MeloNX is open-source and available on Github under the <a href="https://github.com/MeloNX-Emu/MeloNX/blob/master/LICENSE.txt" target="_blank">MeloNX license</a>. <br
</p> </p>
# Compatibility # Compatibility
MeloNX works on iPhone XS/XR and later and iPad 8th Gen and later. Check out the Compatibility on the <a href="https://melonx.org/compatibility/" target="_blank">website</a>. MeloNX works on iPhone 11 (XS/XR may work but can have issues) and later and iPad 8th Gen and later. Check out the Compatibility on the <a href="https://melonx.org/compatibility/" target="_blank">website</a>.
# Usage # Usage
## Paid Certificates are **NOT** supported and we will not give any help when using them.
## 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 cannot be Sideloaded normally and requires the use of the following Installation Guide(s). - MeloNX cannot be Sideloaded normally and requires the use of the following Installation Guide(s).
- [SideStore](https://sidestore.io/) is recommended for Sideloading MeloNX
- Apple ID with free / paid developer account
- MeloNX requires 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.
## Discord Server
We have a discord server!
- https://discord.gg/melonx
## How to install ## How to install
### Paid Developer Account ### Paid Developer Account
1. **Sideload the App** #### 1. Sideload MeloNX
- Use any sideloading tool that supports Apple IDs. Download and install MeloNX using your preferred Apple ID sideloader:
- [Download MeloNX from Releases](https://git.ryujinx.app/melonx/emu/-/releases)
2. **Enable Entitlements** #### 2. Enable Memory Entitlement
- Visit [Apple Developer Identifiers](https://developer.apple.com/account/resources/identifiers). - Visit [Apple Developer Identifiers](https://developer.apple.com/account/resources/identifiers).
- Locate **MeloNX** and enable the following entitlements: - Locate **MeloNX** and enable the following entitlements:
- `Increased Memory Limit` - `Increased Memory Limit`
- `Increased Debugging Memory Limit` - `Increased Debugging Memory Limit`
3. **Reinstall the App** #### 3. Reinstall MeloNX
- Delete the existing installation. - Delete existing MeloNX installation
- Sideload the app again with the updated entitlements. - Sideload MeloNX again
- Verify **Increased Memory Limit** is enabled in app
#### 4. Setup Files
- Add Encryption Keys and Firmware using the file picker inside MeloNX
- If having Issues installing firmware:
- You can Install firmware and keys from **Ryujinx Desktop** (or forks).
- Copy the **bis** and **system** folders
4. **Enable JIT** #### 5. Enable JIT
- Use your preferred method to enable Just-In-Time (JIT) compilation. - Enable JIT using your preferred method. We recommend [StikDebug](https://apps.apple.com/us/app/stikdebug/id6744045754).
- We reccomend using [StikDebug](https://apps.apple.com/us/app/stikdebug/id6744045754)
5. **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
### Free Developer Account (Experimental) ### Free Developer Account
1. **Sideload MeloNX** ***The Entitlement App is **NOT** needed for AltStore Classic***
- Use [SideStore](https://sidestore.io/) or [AltStore](https://altstore.io/) (**NOT** AltStore PAL). - You may skip Step 2 and Step 3
2. **Sideload the Entitlement App** #### 1. Sideload Applications
- 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). Download and install both apps using your preferred **APPLE ID** sideloader:
- **MeloNX**: [Download from Releases](https://git.ryujinx.app/melonx/emu/-/releases)
- **Entitlement App**: [Download IPA](https://github.com/hugeBlack/GetMoreRam/releases/download/nightly/Entitlement.ipa)
3. **Sign In to Your Account** #### 2. Enable Memory Entitlement
- Open **Settings** in the entitlement app and sign in with your Apple ID. - Open the **Entitlement app** > **Settings**
- Sign in with your Apple ID
- Go to **App IDs** > tap **Refresh**
- Select **MeloNX** (e.g., "com.stossy11.MeloNX.XXXXXX")
- Tap **Add Increased Memory Limit**
4. **Refresh App IDs** #### 3. Reinstall MeloNX
- Navigate to the **App IDs** page. - Delete existing MeloNX installation
- Tap **Refresh** to update the list. - Sideload MeloNX again
- Verify **Increased Memory Limit** is enabled in app
5. **Enable Increased Memory Limit** #### 4. Setup Files
- Select **MeloNX** (should be like "com.stossy11.MeloNX" or some variation) from the list. - Add Encryption Keys and Firmware using the file picker inside MeloNX
- Tap **Add Increased Memory Limit**. - If having Issues installing firmware:
- You can Install firmware and keys from **Ryujinx Desktop** (or forks).
- Copy the **bis** and **system** folders
6. **Reinstall MeloNX** #### 5. Enable JIT
- Delete the existing installation. - Enable JIT using your preferred method. We recommend [StikDebug](https://apps.apple.com/us/app/stikdebug/id6744045754).
- 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 [StikDebug](https://apps.apple.com/us/app/stikdebug/id6744045754)
### TrollStore ### TrollStore
As Said in FAQ: 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. > 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** #### 1. Install MeloNX
- Use TrollStore to install MeloNX.
2. **Add Necessary Files** #### 2. Setup Files
- Add Encryption Keys and Firmware using the file picker inside MeloNX
- If having Issues installing firmware:
- You can Install firmware and keys from **Ryujinx Desktop** (or forks).
- Copy the **bis** and **system** folders
3. **Enable TrollStore JIT**
- MeloNX includes automatic JIT using the TrollStore URL Scheme #### 2. Enable TrollStore JIT
- Open MeloNX Settings - Open **Settings** inside **MeloNX**
- Scroll down and enable the "TrollStore JIT" toggle - Under **Misc**, scroll down and enable the **"TrollStore" toggle**
- Profit - Profit
### Free Developer Account (Xcode) ### 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.**
1. **Compile Guide** 1. **Compile Guide**
- Visit the [guide here](https://git.743378673.xyz/MeloNX/MeloNX/src/branch/XC-ios-ht/Compile.md). - Visit the [guide here](https://git.ryujinx.app/melonx/emu/-/blob/XC-ios-ht/Compile.md?ref_type=heads).
2. **Add Necessary Files** 2. **Add Necessary Files**
@ -141,12 +151,12 @@ If having Issues installing firmware (Make sure your keys are installed first)
- **GPU** - **GPU**
The GPU emulator emulates the Switch's Maxwell GPU using Metal (via MoltenVK) APIs through a custom build of OpenTK or Silk.NET respectively. The GPU emulator emulates the Switch's Maxwell GPU using Metal (via MoltenVK) APIs through a custom build of Silk.NET.
- **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 MFI controllers.
Motion controls are natively supported in most cases. Motion controls are natively supported in most cases, however JoyCons do not have motion support doe to an iOS limitation.
- **DLC & Modifications** - **DLC & Modifications**
@ -157,14 +167,13 @@ If having Issues installing firmware (Make sure your keys are installed first)
The emulator has settings for enabling or disabling some logging, remapping controllers, and more. The emulator has settings for enabling or disabling some logging, remapping controllers, and more.
## License # License
This software is licensed under the terms of the [MeloNX license (Based on MIT License)](LICENSE.txt). This software is licensed under the terms of the [MeloNX license](LICENSE.txt).
This project makes use of code authored by the libvpx project, licensed under BSD and the ffmpeg project, licensed under LGPLv3. This project makes use of code authored by the libvpx project, licensed under BSD and the ffmpeg project, licensed under LGPLv3.
See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY.md) for more details. See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY.md) for more details.
## Credits # Credits
- [Ryujinx](https://github.com/ryujinx-mirror/ryujinx) is used for the base of this emulator. (link is to ryujinx-mirror since they were supportive) - [Ryujinx](https://github.com/ryujinx-mirror/ryujinx) is used for the base of this emulator. (link is to ryujinx-mirror since they were supportive)
- [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system. - [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system.
- [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation. - [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation.

View File

@ -47,13 +47,13 @@ namespace ARMeilleure.CodeGen.X86
0xc3, // ret 0xc3, // ret
}; };
using MemoryBlock memGetXcr0 = new((ulong)asmGetXcr0.Length); using MemoryBlock memGetXcr0 = new((ulong)asmGetXcr0.Length, MemoryAllocationFlags.DualMapping);
memGetXcr0.Write(0, asmGetXcr0); memGetXcr0.Write(0, asmGetXcr0);
memGetXcr0.Reprotect(0, (ulong)asmGetXcr0.Length, MemoryPermission.ReadAndExecute); memGetXcr0.Reprotect(0, (ulong)asmGetXcr0.Length, MemoryPermission.ReadAndExecute);
var fGetXcr0 = Marshal.GetDelegateForFunctionPointer<GetXcr0>(memGetXcr0.Pointer); var fGetXcr0 = Marshal.GetDelegateForFunctionPointer<GetXcr0>(memGetXcr0.RxPointer);
return fGetXcr0(); return fGetXcr0();
} }

View File

@ -28,6 +28,7 @@
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 */; };
7CD054B52E253D2F00287A89 /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = 7CD054B42E253D2F00287A89 /* SwiftUIIntrospect */; };
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; }; CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -46,13 +47,6 @@
remoteGlobalIDString = 4E80A98C2CD6F54500029585; remoteGlobalIDString = 4E80A98C2CD6F54500029585;
remoteInfo = MeloNX; remoteInfo = MeloNX;
}; };
4EFFCD182DFB766F00F78EA6 /* 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 */;
@ -109,6 +103,10 @@
CA0AE31D2D3EECBC00F6D350 /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */ = { CA0AE31D2D3EECBC00F6D350 /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */ = {
isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet; isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet;
attributesByRelativePath = { attributesByRelativePath = {
"Dependencies/Dynamic Libraries/BreakpointJIT.framework" = (
CodeSignOnCopy,
RemoveHeadersOnCopy,
);
"Dependencies/Dynamic Libraries/Hypervisor.framework" = ( "Dependencies/Dynamic Libraries/Hypervisor.framework" = (
CodeSignOnCopy, CodeSignOnCopy,
RemoveHeadersOnCopy, RemoveHeadersOnCopy,
@ -120,10 +118,6 @@
CodeSignOnCopy, CodeSignOnCopy,
RemoveHeadersOnCopy, RemoveHeadersOnCopy,
); );
"Dependencies/Dynamic Libraries/StosJIT.framework" = (
CodeSignOnCopy,
RemoveHeadersOnCopy,
);
"Dependencies/Dynamic Libraries/libMoltenVK.dylib" = ( "Dependencies/Dynamic Libraries/libMoltenVK.dylib" = (
CodeSignOnCopy, CodeSignOnCopy,
); );
@ -172,13 +166,13 @@
}; };
buildPhase = 4E80AA092CD6FAA800029585 /* Embed Libraries */; buildPhase = 4E80AA092CD6FAA800029585 /* Embed Libraries */;
membershipExceptions = ( membershipExceptions = (
"Dependencies/Dynamic Libraries/BreakpointJIT.framework",
"Dependencies/Dynamic Libraries/Hypervisor.framework", "Dependencies/Dynamic Libraries/Hypervisor.framework",
"Dependencies/Dynamic Libraries/libavcodec.dylib", "Dependencies/Dynamic Libraries/libavcodec.dylib",
"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/RyujinxHelper.framework", "Dependencies/Dynamic Libraries/RyujinxHelper.framework",
"Dependencies/Dynamic Libraries/StosJIT.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,6 +197,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
7CD054B52E253D2F00287A89 /* SwiftUIIntrospect in Frameworks */,
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */, CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
4549A31C2DD8795900EC8D88 /* CocoaAsyncSocket in Frameworks */, 4549A31C2DD8795900EC8D88 /* CocoaAsyncSocket in Frameworks */,
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */, 4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
@ -294,7 +289,6 @@
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
4EFFCD192DFB766F00F78EA6 /* PBXTargetDependency */,
); );
fileSystemSynchronizedGroups = ( fileSystemSynchronizedGroups = (
4E80A98F2CD6F54500029585 /* MeloNX */, 4E80A98F2CD6F54500029585 /* MeloNX */,
@ -303,6 +297,7 @@
packageProductDependencies = ( packageProductDependencies = (
4EA5AE812D16807500AD0B9F /* SwiftSVG */, 4EA5AE812D16807500AD0B9F /* SwiftSVG */,
4549A31B2DD8795900EC8D88 /* CocoaAsyncSocket */, 4549A31B2DD8795900EC8D88 /* CocoaAsyncSocket */,
7CD054B42E253D2F00287A89 /* SwiftUIIntrospect */,
); );
productName = MeloNX; productName = MeloNX;
productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */; productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */;
@ -395,6 +390,7 @@
packageReferences = ( packageReferences = (
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */, 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */,
4549A31A2DD8795900EC8D88 /* XCRemoteSwiftPackageReference "CocoaAsyncSocket" */, 4549A31A2DD8795900EC8D88 /* XCRemoteSwiftPackageReference "CocoaAsyncSocket" */,
7CD054B32E253D2F00287A89 /* XCRemoteSwiftPackageReference "swiftui-introspect" */,
); );
preferredProjectObjectVersion = 56; preferredProjectObjectVersion = 56;
productRefGroup = 4E80A98E2CD6F54500029585 /* Products */; productRefGroup = 4E80A98E2CD6F54500029585 /* Products */;
@ -492,11 +488,6 @@
target = 4E80A98C2CD6F54500029585 /* MeloNX */; target = 4E80A98C2CD6F54500029585 /* MeloNX */;
targetProxy = 4E80A9A82CD6F54700029585 /* PBXContainerItemProxy */; targetProxy = 4E80A9A82CD6F54700029585 /* PBXContainerItemProxy */;
}; };
4EFFCD192DFB766F00F78EA6 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
targetProxy = 4EFFCD182DFB766F00F78EA6 /* PBXContainerItemProxy */;
};
BD43C6262D1B249E003BBC42 /* PBXTargetDependency */ = { BD43C6262D1B249E003BBC42 /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
target = BD43C61D2D1B23AB003BBC42 /* Ryujinx */; target = BD43C61D2D1B23AB003BBC42 /* Ryujinx */;
@ -655,7 +646,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8; DEVELOPMENT_TEAM = D59DHVRS87;
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO; EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = NO; ENABLE_TESTABILITY = NO;
@ -791,6 +782,13 @@
"$(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 = z; GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -1046,9 +1044,19 @@
"$(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.xitrix.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
@ -1071,7 +1079,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8; DEVELOPMENT_TEAM = D59DHVRS87;
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO; EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
@ -1207,6 +1215,13 @@
"$(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 = z; GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -1462,9 +1477,19 @@
"$(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.xitrix.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
@ -1679,6 +1704,14 @@
kind = branch; kind = branch;
}; };
}; };
7CD054B32E253D2F00287A89 /* XCRemoteSwiftPackageReference "swiftui-introspect" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/siteline/swiftui-introspect.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.3.0;
};
};
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
@ -1692,6 +1725,11 @@
package = 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */; package = 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */;
productName = SwiftSVG; productName = SwiftSVG;
}; };
7CD054B42E253D2F00287A89 /* SwiftUIIntrospect */ = {
isa = XCSwiftPackageProductDependency;
package = 7CD054B32E253D2F00287A89 /* XCRemoteSwiftPackageReference "swiftui-introspect" */;
productName = SwiftUIIntrospect;
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
}; };
rootObject = 4E80A9852CD6F54500029585 /* Project object */; rootObject = 4E80A9852CD6F54500029585 /* Project object */;

View File

@ -1,5 +1,5 @@
{ {
"originHash" : "b4a593815773c4e9eedb98cabe88f41620776314bffb6c39d5a41cb743e4d390", "originHash" : "ae170c69ea1ccacb2f3982b1a91b689a09bc4dcc59ed970f1df977bf7c7aed6f",
"pins" : [ "pins" : [
{ {
"identity" : "cocoaasyncsocket", "identity" : "cocoaasyncsocket",
@ -18,6 +18,15 @@
"branch" : "master", "branch" : "master",
"revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d" "revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d"
} }
},
{
"identity" : "swiftui-introspect",
"kind" : "remoteSourceControl",
"location" : "https://github.com/siteline/swiftui-introspect.git",
"state" : {
"revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336",
"version" : "1.3.0"
}
} }
], ],
"version" : 3 "version" : 3

View File

@ -65,6 +65,7 @@
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugXPCServices = "NO" debugXPCServices = "NO"
debugServiceExtension = "internal" debugServiceExtension = "internal"
enableGPUFrameCaptureMode = "3"
enableGPUValidationMode = "1" enableGPUValidationMode = "1"
allowLocationSimulation = "YES" allowLocationSimulation = "YES"
queueDebuggingEnabled = "No" queueDebuggingEnabled = "No"

View File

@ -0,0 +1,52 @@
//
// MobileGestalt.h
// MeloNX
//
// Created by Stossy11 on 11/07/2025.
//
/*
* libMobileGestalt header.
* Mobile gestalt functions as a QA system. You ask it a question, and it gives you the answer! :)
*
* Copyright (c) 2013-2014 Cykey (David Murray)
* Improved by @PoomSmart (2020)
* All rights reserved.
*/
#ifndef LIBMOBILEGESTALT_H_
#define LIBMOBILEGESTALT_H_
#include <dlfcn.h>
#include <CoreFoundation/CoreFoundation.h>
#if __cplusplus
extern "C" {
#endif
#pragma mark - API
typedef CFPropertyListRef (*MGFuncType)(CFStringRef);
CFPropertyListRef CallMGCopyAnswer(CFStringRef key) {
static MGFuncType fn = NULL;
if (!fn) {
void *handle = dlopen("/usr/lib/libMobileGestalt.dylib", RTLD_LAZY);
if (!handle) return NULL;
char decoded[] = { 'M','G','C','o','p','y','A','n','s','w','e','r', '\0' };
fn = (MGFuncType)dlsym(handle, decoded);
}
return fn ? fn(key) : NULL;
}
#pragma mark - Device Information
static const CFStringRef kMGPhysicalHardwareNameString = CFSTR("PhysicalHardwareNameString");
#if __cplusplus
}
#endif
#endif /* LIBMOBILEGESTALT_H_ */

View File

@ -14,8 +14,7 @@
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <SDL2/SDL_syswm.h> #include <SDL2/SDL_syswm.h>
#include <StosJIT/StosJIT-Swift.h> #include "MobileGestalt.h"
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {

View File

@ -17,7 +17,11 @@ func isJITEnabled() -> Bool {
return allocateTest() return allocateTest()
} }
return csops(pid: getpid(), ops: 0, useraddr: &flags, usersize: Int32(MemoryLayout.size(ofValue: flags))) == 0 && (flags & Int(CS_DEBUGGED)) != 0 ? allocateTest() : false if #available(iOS 19, *) {
return checkDebugged()
} else {
return checkDebugged() && allocateTest()
}
} }
func checkDebugged() -> Bool { func checkDebugged() -> Bool {
@ -62,7 +66,6 @@ func allocateTest() -> Bool {
memcpy(jitMemory, code, code.count) memcpy(jitMemory, code, code.count)
if mprotect(jitMemory, pageSize, PROT_READ | PROT_EXEC) != 0 { if mprotect(jitMemory, pageSize, PROT_READ | PROT_EXEC) != 0 {
return false return false
} }
@ -71,3 +74,26 @@ func allocateTest() -> Bool {
return checkMem return checkMem
} }
// thank you nikki (nythepegasus)
extension FileManager {
func filePath(atPath path: String, withLength length: Int) -> String? {
guard let file = try? contentsOfDirectory(atPath: path).filter({ $0.count == length }).first else { return nil }
return "\(path)/\(file)"
}
}
func notnil(_ condition: Any?) -> Bool {
if let _ = condition {
return false
} else {
return true
}
}
public extension ProcessInfo {
var hasTXM: Bool {
{ if let boot = FileManager.default.filePath(atPath: "/System/Volumes/Preboot", withLength: 36), let file = FileManager.default.filePath(atPath: "\(boot)/boot", withLength: 96) { return access("\(file)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0 } else { return (FileManager.default.filePath(atPath: "/private/preboot", withLength: 96).map { access("\($0)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0 }) ?? false } }()
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,11 @@
//
// ControllerInfo.swift
// MeloNX
//
// Created by Stossy11 on 28/06/2025.
//
struct Controller: Identifiable, Hashable {
var id: String
var name: String
}

View File

@ -22,21 +22,21 @@ class NativeController: Hashable, BaseController {
init(_ controller: GCController) { init(_ controller: GCController) {
nativeController = controller nativeController = controller
controllerHaptics = nativeController.haptics?.createEngine(withLocality: .default) var ncontrollerHaptics = nativeController.haptics?.createEngine(withLocality: .default)
let vendorName = nativeController.vendorName ?? "Unknown"
var usesdeviceHaptics = (ncontrollerHaptics == nil && (vendorName.lowercased().hasSuffix("backbone") || vendorName.lowercased() == "backbone one"))
controllerHaptics = usesdeviceHaptics ? try? CHHapticEngine() : ncontrollerHaptics
// Make sure the haptic engine exists before attempting to start it or initialize the controller. // Make sure the haptic engine exists before attempting to start it or initialize the controller.
if let hapticsEngine = controllerHaptics { if let hapticsEngine = controllerHaptics {
do { do {
try hapticsEngine.start() try hapticsEngine.start()
rumbleController = RumbleController(engine: hapticsEngine, rumbleMultiplier: 2.5) rumbleController = RumbleController(engine: hapticsEngine, rumbleMultiplier: usesdeviceHaptics ? 2.0 : 2.5)
// print("CHHapticEngine started and RumbleController initialized.")
} catch { } catch {
// print("Error starting CHHapticEngine: \(error.localizedDescription)")
rumbleController = nil rumbleController = nil
} }
} else { } else {
// print("CHHapticEngine is nil. Cannot initialize RumbleController.")
rumbleController = nil rumbleController = nil
} }
setupHandheldController() setupHandheldController()
@ -49,26 +49,24 @@ class NativeController: Hashable, BaseController {
internal func tryRegisterMotion(slot: UInt8) { internal func tryRegisterMotion(slot: UInt8) {
// Setup Motion // Setup Motion
let dsuServer = DSUServer.shared let dsuServer = DSUServer.shared
let vendorName = nativeController.vendorName ?? "Unknown"
var usesdevicemotion = (vendorName.lowercased() == "Joy-Con (l/R)".lowercased() || vendorName.lowercased().hasSuffix("backbone") || vendorName.lowercased() == "backbone one")
if nativeController.vendorName?.lowercased() == "Joy-Con (l/R)".lowercased() { usesdevicemotion ? (deviceMotionProvider = DeviceMotionProvider(slot: slot)) : (controllerMotionProvider = ControllerMotionProvider(controller: nativeController, slot: slot))
deviceMotionProvider = DeviceMotionProvider(slot: slot)
if let provider = deviceMotionProvider { if let provider = controllerMotionProvider {
dsuServer.register(provider) dsuServer.register(provider)
} } else if let provider = deviceMotionProvider {
} else { dsuServer.register(provider)
controllerMotionProvider = ControllerMotionProvider(controller: nativeController, slot: slot)
if let provider = controllerMotionProvider {
dsuServer.register(provider)
}
} }
} }
internal func tryGetMotionProvider() -> DSUMotionProvider? { internal func tryGetMotionProvider() -> DSUMotionProvider? {
if nativeController.vendorName == "Joy-Con (l/R)" { if let deviceMotionProvider {
return deviceMotionProvider return deviceMotionProvider
} else {
return controllerMotionProvider
} }
return controllerMotionProvider
} }
private func setupHandheldController() { private func setupHandheldController() {
@ -94,38 +92,39 @@ class NativeController: Hashable, BaseController {
}, },
SetPlayerIndex: { userdata, playerIndex in SetPlayerIndex: { userdata, playerIndex in
// print("Player index set to \(playerIndex)") // print("Player index set to \(playerIndex)")
guard let userdata, let player = GCControllerPlayerIndex(rawValue: Int(playerIndex)) else { return }
let _self = Unmanaged<NativeController>.fromOpaque(userdata).takeUnretainedValue()
_self.nativeController.playerIndex = player
}, },
Rumble: { userdata, lowFreq, highFreq in Rumble: { userdata, lowFreq, highFreq in
// print("Rumble with \(lowFreq), \(highFreq)")
guard let userdata else { return 0 } guard let userdata else { return 0 }
let _self = Unmanaged<NativeController>.fromOpaque(userdata).takeUnretainedValue() let _self = Unmanaged<NativeController>.fromOpaque(userdata).takeUnretainedValue()
_self.rumbleController?.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq)) _self.rumbleController?.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
return 0 return 0
}, },
RumbleTriggers: { userdata, leftRumble, rightRumble in RumbleTriggers: { userdata, leftRumble, rightRumble in
// print("Trigger rumble with \(leftRumble), \(rightRumble)")
return 0 return 0
}, },
SetLED: { userdata, red, green, blue in SetLED: { userdata, red, green, blue in
// print("Set LED to RGB(\(red), \(green), \(blue))") guard let userdata else { return 0 }
let _self = Unmanaged<NativeController>.fromOpaque(userdata).takeUnretainedValue()
guard let light = _self.nativeController.light else { return 0 }
light.color = .init(red: Float(red), green: Float(green), blue: Float(blue))
return 0 return 0
}, },
SendEffect: { userdata, data, size in SendEffect: { userdata, data, size in
// print("Effect sent with size \(size)")
return 0 return 0
} }
) )
instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1) instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)
if instanceID < 0 { if instanceID < 0 {
// print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
return return
} }
controller = SDL_GameControllerOpen(Int32(instanceID)) controller = SDL_GameControllerOpen(Int32(instanceID))
if controller == nil { if controller == nil {
// print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
return return
} }
@ -214,7 +213,6 @@ class NativeController: Hashable, BaseController {
func setButtonState(_ state: Uint8, for button: VirtualControllerButton) { func setButtonState(_ state: Uint8, for button: VirtualControllerButton) {
guard controller != nil else { return } guard controller != nil else { return }
// // print("Button: \(button.rawValue) {state: \(state)}")
if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) { if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) {
let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
let value: Int = (state == 1) ? 32767 : 0 let value: Int = (state == 1) ? 32767 : 0

View File

@ -158,7 +158,7 @@ class VirtualController : BaseController {
} }
} }
enum VirtualControllerButton: Int { enum VirtualControllerButton: Int, Codable {
case A case A
case B case B
case X case X
@ -196,7 +196,7 @@ enum VirtualControllerButton: Int {
} }
} }
enum ThumbstickType: Int { enum ThumbstickType: Int, Codable {
case left case left
case right case right
} }

View File

@ -99,10 +99,7 @@ extension Notification.Name {
static let newLogCaptured = Notification.Name("newLogCaptured") static let newLogCaptured = Notification.Name("newLogCaptured")
} }
struct Controller: Identifiable, Hashable {
var id: String
var name: String
}
struct iOSNav<Content: View>: View { struct iOSNav<Content: View>: View {
@ViewBuilder var content: () -> Content @ViewBuilder var content: () -> Content
@ -136,6 +133,7 @@ class Ryujinx : ObservableObject {
@Published var emulationUIView: MeloMTKView? = nil @Published var emulationUIView: MeloMTKView? = nil
@Published var config: Ryujinx.Arguments? = nil @Published var config: Ryujinx.Arguments? = nil
@Published var games: [Game] = [] @Published var games: [Game] = []
@Published var aspectRatio: AspectRatio = .fixed16x9
@Published var defMLContentSize: CGFloat? @Published var defMLContentSize: CGFloat?
@ -240,7 +238,7 @@ class Ryujinx : ObservableObject {
disablevsync: Bool = false, disablevsync: Bool = false,
language: SystemLanguage = .americanEnglish, language: SystemLanguage = .americanEnglish,
regioncode: SystemRegionCode = .usa, regioncode: SystemRegionCode = .usa,
handHeldController: Bool = false, handHeldController: Bool = false
) { ) {
self.gamepath = gamepath self.gamepath = gamepath
self.inputids = inputids self.inputids = inputids
@ -424,6 +422,42 @@ class Ryujinx : ObservableObject {
} }
} }
static func clearShaderCache(_ titleId: String = "") {
showAlert(title: "Clear Shader Cache", message: titleId.isEmpty ? "Are you sure you want to clear ALL shader cache?" : "Are you sure you want to clear your shader cache?",
actions: [
(title: "Cancel", style: .cancel, handler: nil),
(title: "Clear", style: .destructive, handler: {
if titleId.isEmpty {
let fileManager = FileManager.default
let gamesURL = URL.documentsDirectory.appendingPathComponent("games")
do {
let contents = try fileManager.contentsOfDirectory(at: gamesURL, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles])
let folderURLs = contents.filter { url in
(try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) == true
}
for folderURL in folderURLs {
try? fileManager.removeItem(at: folderURL.appendingPathComponent("cache"))
}
} catch {
print("Error reading games folder: \(error)")
}
} else {
let fileManager = FileManager.default
let cacheURL = URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(titleId).appendingPathComponent("cache")
try? fileManager.removeItem(at: cacheURL)
}
}),
]
)
}
struct ExceptionInfo { struct ExceptionInfo {
let exceptionType: String let exceptionType: String
let message: String let message: String
@ -563,7 +597,11 @@ class Ryujinx : ObservableObject {
args.append(contentsOf: ["--system-region", config.regioncode.rawValue]) args.append(contentsOf: ["--system-region", config.regioncode.rawValue])
args.append(contentsOf: ["--aspect-ratio", config.aspectRatio.rawValue]) DispatchQueue.main.async {
self.aspectRatio = config.aspectRatio
}
args.append(contentsOf: ["--aspect-ratio", "Stretched"])
args.append(contentsOf: ["--system-timezone", TimeZone.current.identifier]) args.append(contentsOf: ["--system-timezone", TimeZone.current.identifier])
@ -835,116 +873,8 @@ public extension UIDevice {
return identifier + String(UnicodeScalar(UInt8(value))) return identifier + String(UnicodeScalar(UInt8(value)))
} }
func mapToDevice(identifier: String) -> String { // swiftlint:disable:this cyclomatic_complexity return CallMGCopyAnswer(kMGPhysicalHardwareNameString)?.takeUnretainedValue() as? String ?? identifier
#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

@ -1,27 +0,0 @@
//
// ToggleButtonsState.swift
// MeloNX
//
// Created by Stossy11 on 12/04/2025.
//
struct ToggleButtonsState: Codable, Equatable {
var toggle1: Bool
var toggle2: Bool
var toggle3: Bool
var toggle4: Bool
init() {
self = .default
}
init(toggle1: Bool, toggle2: Bool, toggle3: Bool, toggle4: Bool) {
self.toggle1 = toggle1
self.toggle2 = toggle2
self.toggle3 = toggle3
self.toggle4 = toggle4
}
static let `default` = ToggleButtonsState(toggle1: false, toggle2: false, toggle3: false, toggle4: false)
}

View File

@ -0,0 +1,83 @@
//
// NavigationItemPalette.swift
// iTorrent
//
// Created by Daniil Vinogradov on 14.11.2024.
//
import UIKit
import SwiftUI
@_spi(Advanced) import SwiftUIIntrospect
public extension View {
func navigationItemBottomPalette(@ViewBuilder body: () -> (some View)) -> some View {
modifier(NavitaionItemBottomPaletteContent(content: body().asController))
}
}
struct NavitaionItemBottomPaletteContent: ViewModifier {
let content: UIViewController
func body(content: Content) -> some View {
content
.introspect(.viewController, on: .iOS(.v14...), customize: { viewController in
let view = self.content.view!
view.backgroundColor = .clear
let size = view.systemLayoutSizeFitting(.init(width: viewController.view.frame.width, height: UIView.layoutFittingCompressedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
viewController.navigationItem.setBottomPalette(view, height: size.height)
})
}
}
extension UINavigationItem {
func setBottomPalette(_ contentView: UIView?, height: CGFloat = 44) {
/// "_setBottomPalette:"
let selector = NSSelectorFromBase64String("X3NldEJvdHRvbVBhbGV0dGU6")
guard responds(to: selector) else { return }
perform(selector, with: Self.makeNavigationItemPalette(with: contentView, height: height))
}
private static func makeNavigationItemPalette(with contentView: UIView?, height: CGFloat) -> UIView? {
guard let contentView else { return nil }
contentView.translatesAutoresizingMaskIntoConstraints = false
let contentViewHolder = UIView(frame: .init(x: 0, y: 0, width: 0, height: height))
contentViewHolder.autoresizingMask = [.flexibleHeight]
contentViewHolder.addSubview(contentView)
NSLayoutConstraint.activate([
contentViewHolder.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
contentViewHolder.topAnchor.constraint(equalTo: contentView.topAnchor),
contentView.trailingAnchor.constraint(equalTo: contentViewHolder.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: contentViewHolder.bottomAnchor),
])
/// "_UINavigationBarPalette"
guard let paletteClass = NSClassFromBase64String("X1VJTmF2aWdhdGlvbkJhclBhbGV0dGU=") as? UIView.Type
else { return nil }
/// "alloc"
/// "initWithContentView:"
guard let palette = paletteClass.perform(NSSelectorFromBase64String("YWxsb2M="))
.takeUnretainedValue()
.perform(NSSelectorFromBase64String("aW5pdFdpdGhDb250ZW50Vmlldzo="), with: contentViewHolder)
.takeUnretainedValue() as? UIView
else { return nil }
palette.preservesSuperviewLayoutMargins = true
return palette
}
}
func NSSelectorFromBase64String(_ base64String: String) -> Selector {
NSSelectorFromString(String(base64: base64String))
}
func NSClassFromBase64String(_ aBase64ClassName: String) -> AnyClass? {
NSClassFromString(String(base64: aBase64ClassName))
}
extension String {
init(base64: String) {
self.init(data: Data(base64Encoded: base64)!, encoding: .utf8)!
}
}

View File

@ -0,0 +1,36 @@
//
// UIKitSwiftUIInarop.swift
// iTorrent
//
// Created by Daniil Vinogradov on 01/11/2023.
//
import SwiftUI
private struct GenericControllerView: UIViewControllerRepresentable {
let viewController: UIViewController
typealias UIViewControllerType = UIViewController
func makeUIViewController(context: Context) -> UIViewController {
viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) { /* Ignore */ }
}
extension View {
@MainActor
var asController: UIHostingController<Self> {
let vc = UIHostingController<Self>(rootView: self)
if #available(iOS 16.4, *) {
vc.safeAreaRegions = []
}
return vc
}
}
extension UIViewController {
var asView: some View {
GenericControllerView(viewController: self)
}
}

View File

@ -0,0 +1,27 @@
//
// Alerts.swift
// MeloNX
//
// Created by Stossy11 on 04/07/2025.
//
import UIKit
func showAlert(_ viewController: UIViewController? = nil,
title: String?,
message: String?,
actions: [(title: String, style: UIAlertAction.Style, handler: (() -> Void)?)]) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
for action in actions {
let uiAction = UIAlertAction(title: action.title, style: action.style) { _ in
action.handler?()
}
alert.addAction(uiAction)
}
let coolVC = viewController ?? UIApplication.shared.windows.first?.rootViewController!
coolVC!.present(alert, animated: true, completion: nil)
}

View File

@ -10,22 +10,23 @@ import SwiftUI
struct Joystick: View { struct Joystick: View {
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
@State var right = true
@Binding var position: CGPoint @Binding var position: CGPoint
@State var joystickSize: CGFloat @State var joystickSize: CGFloat
var boundarySize: CGFloat var boundarySize: CGFloat
@State private var offset: CGSize = .zero @State private var offset: CGSize = .zero
@Binding var showBackground: Bool @Binding var showBackground: Bool
@State var joystickSmallSize = false
let sensitivity: CGFloat = 1.2 let sensitivity: CGFloat = 1.2
var dragGesture: some Gesture { var dragGesture: some Gesture {
DragGesture() DragGesture()
.onChanged { value in .onChanged { value in
withAnimation(.easeIn) { withAnimation(.easeIn) {
showBackground = true showBackground = true
joystickSmallSize = true
} }
let translation = value.translation let translation = value.translation
@ -44,16 +45,28 @@ struct Joystick: View {
x: max(-1, min(1, (offset.width / extendedRadius) * sensitivity)), x: max(-1, min(1, (offset.width / extendedRadius) * sensitivity)),
y: max(-1, min(1, (offset.height / extendedRadius) * sensitivity)) y: max(-1, min(1, (offset.height / extendedRadius) * sensitivity))
) )
setPos()
} }
.onEnded { _ in .onEnded { _ in
offset = .zero offset = .zero
position = .zero position = .zero
setPos()
withAnimation(.easeOut) { withAnimation(.easeOut) {
showBackground = false showBackground = false
joystickSmallSize = false
} }
} }
} }
func setPos() {
if right {
Ryujinx.shared.virtualController.thumbstickMoved(.right, x: position.x, y: position.y)
} else {
Ryujinx.shared.virtualController.thumbstickMoved(.left, x: position.x, y: position.y)
}
}
var body: some View { var body: some View {
ZStack { ZStack {
Circle() Circle()
@ -82,7 +95,7 @@ struct Joystick: View {
.scaleEffect(controllerScale) .scaleEffect(controllerScale)
} }
.frame(width: boundarySize, height: boundarySize) .frame(width: boundarySize, height: boundarySize)
.onChange(of: showBackground) { newValue in .onChange(of: joystickSmallSize) { newValue in
if newValue { if newValue {
joystickSize *= 1.4 joystickSize *= 1.4
} else { } else {

View File

@ -9,7 +9,7 @@
import SwiftUI import SwiftUI
struct JoystickController: View { struct JoystickController: View {
@State var iscool: Bool? = nil @State var iscool: Bool
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@Binding var showBackground: Bool @Binding var showBackground: Bool
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
@ -25,15 +25,8 @@ struct JoystickController: View {
} }
public var body: some View { public var body: some View {
VStack { Group {
Joystick(position: $position, joystickSize: dragDiameter * 0.2, boundarySize: dragDiameter, showBackground: $showBackground) Joystick(right: iscool, position: $position, joystickSize: dragDiameter * 0.2, boundarySize: dragDiameter, showBackground: $showBackground)
.onChange(of: position) { newValue in
if iscool != nil {
Ryujinx.shared.virtualController.thumbstickMoved(.right, x: newValue.x, y: newValue.y)
} else {
Ryujinx.shared.virtualController.thumbstickMoved(.left, x: newValue.x, y: newValue.y)
}
}
} }
} }
} }

View File

@ -16,8 +16,7 @@ struct EmulationView: View {
@AppStorage("On-ScreenControllerOpacity") var controllerOpacity: Double = 1.0 @AppStorage("On-ScreenControllerOpacity") var controllerOpacity: Double = 1.0
@AppStorage("disableTouch") var blackScreen = false @AppStorage("OldView") var oldView = true
@State var isPresentedThree: Bool = false @State var isPresentedThree: Bool = false
@State var isAirplaying = Air.shared.connected @State var isAirplaying = Air.shared.connected
@Binding var startgame: Game? @Binding var startgame: Game?
@ -27,9 +26,18 @@ struct EmulationView: View {
@State var showSettings = false @State var showSettings = false
@State var pauseEmu = true @State var pauseEmu = true
@AppStorage("location-enabled") var locationenabled: Bool = false @AppStorage("location-enabled") var locationenabled: Bool = false
@FocusState private var isFocused: Bool
@ObservedObject var ryujijnx = Ryujinx.shared
var body: some View { var body: some View {
ZStack { ZStack {
if oldView {
Color.black
.ignoresSafeArea()
.edgesIgnoringSafeArea(.all)
.allowsHitTesting(false)
}
if isAirplaying { if isAirplaying {
TouchView() TouchView()
.ignoresSafeArea() .ignoresSafeArea()
@ -37,104 +45,108 @@ struct EmulationView: View {
.onAppear { .onAppear {
Air.play(AnyView(MetalView().ignoresSafeArea().edgesIgnoringSafeArea(.all))) Air.play(AnyView(MetalView().ignoresSafeArea().edgesIgnoringSafeArea(.all)))
} }
Color.black
.ignoresSafeArea()
.edgesIgnoringSafeArea(.all)
.allowsHitTesting(false)
} else { } else {
MetalView() // The Emulation View MetalViewContainer() // The Emulation View
.ignoresSafeArea()
.edgesIgnoringSafeArea(.all)
} }
// Above Emulation View // Above Emulation View
if isVCA { if isVCA {
ControllerView() // Virtual Controller ControllerView(isEditing: .constant(false), gameId: startgame?.titleId) // Virtual Controller
.opacity(controllerOpacity) .opacity(controllerOpacity)
.allowsHitTesting(true) .allowsHitTesting(true)
} }
Group {
VStack { VStack {
HStack { HStack {
if performacehud, !showlogsgame { if !performacehud, showlogsgame, ProcessInfo.processInfo.isLowPowerModeEnabled {
PerformanceOverlayView() Circle()
} .fill(Color.orange)
.frame(width: 10, height: 10)
Spacer() .padding()
if performacehud, showlogsgame {
PerformanceOverlayView()
}
} }
HStack {
if showlogsgame, get_current_fps() != 0 {
LogFileView(isfps: false)
}
Spacer()
}
if ssb {
HStack {
Menu {
/*
Button {
showSettings.toggle()
} label: { if ssb {
Label { Menu {
Text("Game Settings") Button {
} icon: { pause_emulation(pauseEmu)
Image(systemName: "gearshape.circle") pauseEmu.toggle()
} } label: {
Label {
Text(pauseEmu ? "Pause" : "Play")
} icon: {
Image(systemName: pauseEmu ? "pause.circle" : "play.circle")
} }
*/ }
Menu() {
Button { ForEach(AspectRatio.allCases.filter { $0 != ryujijnx.aspectRatio }, id: \.self) { ratio in
pause_emulation(pauseEmu) Button {
pauseEmu.toggle() ryujijnx.aspectRatio = ratio
} label: { } label: {
Label { Label {
Text(pauseEmu ? "Pause" : "Play") Text(ratio.displayName)
} icon: { } icon: {
Image(systemName: pauseEmu ? "pause.circle" : "play.circle") Image(systemName: "rectangle.expand.vertical")
} }
}
Button(role: .destructive) {
startgame = nil
stop_emulation()
try? Ryujinx.shared.stop()
} label: {
Label {
Text("Exit (Unstable)")
} icon: {
Image(systemName: "x.circle")
} }
} }
} label: { } label: {
ExtButtonIconView(button: .guide, opacity: 0.4) Label {
Text(ryujijnx.aspectRatio.displayName)
} icon: {
Image(systemName: "rectangle.expand.vertical")
}
} }
.padding()
Spacer() Button(role: .destructive) {
startgame = nil
stop_emulation()
try? Ryujinx.shared.stop()
} label: {
Label {
Text("Exit (Unstable)")
} icon: {
Image(systemName: "x.circle")
}
}
} label: {
ExtButtonIconView(button: .guide, opacity: 0.4)
} }
.menuStyle(.borderlessButton)
.menuIndicator(.hidden)
.padding()
} }
Spacer() Spacer()
if performacehud, getenv("MTL_HUD_ENABLED").flatMap({ String(cString: $0) }) != "1" {
PerformanceOverlayView()
.opacity(controllerOpacity)
.padding(.horizontal)
}
}
Spacer()
}
if showlogsgame, get_current_fps() != 0 {
VStack {
LogFileView(isfps: false)
Spacer()
} }
} }
} }
.onAppear { .onAppear {
DispatchQueue.main.async {
isFocused = true
}
LocationManager.sharedInstance.startUpdatingLocation() LocationManager.sharedInstance.startUpdatingLocation()
Air.shared.connectionCallbacks.append { cool in Air.shared.connectionCallbacks.append { cool in
DispatchQueue.main.async { DispatchQueue.main.async {
@ -148,10 +160,12 @@ struct EmulationView: View {
print(cool) print(cool)
startgame = nil startgame = nil
stop_emulation() stop_emulation()
try? Ryujinx.shared.stop() try? ryujijnx.stop()
} }
} }
} }
.onKeyPress()
.focused($isFocused)
.onChange(of: scenePhase) { newPhase in .onChange(of: scenePhase) { newPhase in
// Detect when the app enters the background // Detect when the app enters the background
if newPhase == .background { if newPhase == .background {
@ -173,3 +187,20 @@ struct EmulationView: View {
} }
} }
} }
// This is just to stop the sound on macOS when doing a keypress
extension View {
func onKeyPress() -> some View {
if #available(iOS 17.0, *), ProcessInfo.processInfo.isiOSAppOnMac {
return AnyView(self
.focusable()
.focusEffectDisabled()
.onKeyPress { _ in
return .handled
})
} else {
return AnyView(self)
}
}
}

View File

@ -35,7 +35,6 @@ class InGameSettingsManager: PerGameSettingsManaging {
Ryujinx.shared.config = config[currentgame.titleId] Ryujinx.shared.config = config[currentgame.titleId]
let args = Ryujinx.shared.buildCommandLineArgs(from: config[currentgame.titleId] ?? Ryujinx.Arguments()) let args = Ryujinx.shared.buildCommandLineArgs(from: config[currentgame.titleId] ?? Ryujinx.Arguments())
// Convert Arguments to ones that Ryujinx can Read
let cArgs = args.map { strdup($0) } let cArgs = args.map { strdup($0) }
defer { cArgs.forEach { free($0) } } defer { cArgs.forEach { free($0) } }
var argvPtrs = cArgs var argvPtrs = cArgs

View File

@ -14,12 +14,21 @@ struct PerformanceOverlayView: View {
var body: some View { var body: some View {
VStack { VStack {
Text("\(fpsmonitor.formatFPS())") if ProcessInfo.processInfo.isLowPowerModeEnabled {
.foregroundStyle(.white) Text("\(fpsmonitor.formatFPS())")
.stroke(color: .black, width: 2) .foregroundStyle(.white)
Text(memorymonitor.formatMemorySize(memorymonitor.memoryUsage)) .stroke(color: .orange, width: 2)
.foregroundStyle(.white) Text(memorymonitor.formatMemorySize(memorymonitor.memoryUsage))
.stroke(color: .black, width: 2) .foregroundStyle(.white)
.stroke(color: .orange, width: 2)
} else {
Text("\(fpsmonitor.formatFPS())")
.foregroundStyle(.white)
.stroke(color: .black, width: 2)
Text(memorymonitor.formatMemorySize(memorymonitor.memoryUsage))
.foregroundStyle(.white)
.stroke(color: .black, width: 2)
}
} }
} }
} }

View File

@ -0,0 +1,154 @@
//
// MetalViewContainer.swift
// MeloNX
//
// Created by Stossy11 on 06/07/2025.
//
import SwiftUI
struct MetalViewContainer: View {
@ObservedObject var ryujinx = Ryujinx.shared
@AppStorage("OldView") var oldView = true
@Environment(\.colorScheme) var colorScheme
@Environment(\.horizontalSizeClass) var horizontalSizeClass
var body: some View {
GeometryReader { geo in
ZStack {
if shouldStretchToFillScreen {
Color.black
stretchedView(containerSize: geo.size)
} else if oldView {
Color.black
oldStyleView(containerSize: geo.size)
} else {
modernView(containerSize: geo.size)
}
}
.animation(.easeInOut(duration: 0.3), value: ryujinx.aspectRatio)
.animation(.easeInOut(duration: 0.3), value: oldView)
}
.ignoresSafeArea()
}
// MARK: - View Components
private func stretchedView(containerSize: CGSize) -> some View {
let size = targetSize(for: containerSize, ignoreSafeArea: true)
return MetalView()
.frame(width: size.width, height: size.height)
}
private func oldStyleView(containerSize: CGSize) -> some View {
let size = targetSize(for: containerSize)
let isPortrait = containerSize.width < containerSize.height
return ZStack {
MetalView()
.frame(width: size.width, height: size.height)
.aspectRatio(contentMode: .fit)
.ignoresSafeArea(.container, edges: isPortrait ? .horizontal : .vertical)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
private func modernView(containerSize: CGSize) -> some View {
let size = targetSize(for: containerSize)
let isPortrait = containerSize.width < containerSize.height
let isPhone = UIDevice.current.userInterfaceIdiom == .phone
let scale = calculateScale(isPortrait: isPortrait, isPhone: isPhone)
return ZStack {
borderedMetalView(size: size)
.scaleEffect(scale)
.ignoresSafeArea(.container, edges: isPortrait ? .horizontal : .vertical)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
private func borderedMetalView(size: CGSize) -> some View {
let cornerRadius: CGFloat = 16
let borderWidth: CGFloat = 2
return ZStack {
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.fill(backgroundColor)
.shadow(color: shadowColor, radius: 8, x: 0, y: 2)
MetalView()
.frame(width: size.width - borderWidth * 2, height: size.height - borderWidth * 2)
.clipShape(RoundedRectangle(cornerRadius: cornerRadius - borderWidth, style: .continuous))
}
.overlay(
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.strokeBorder(borderColor, lineWidth: borderWidth)
)
.frame(width: size.width, height: size.height)
}
// MARK: - Computed Properties
private var shouldStretchToFillScreen: Bool {
ryujinx.aspectRatio == .stretched ||
(ryujinx.aspectRatio == .fixed4x3 && isScreenAspectRatio(4, 3))
}
private var borderColor: Color {
colorScheme == .light ? Color.gray : Color(UIColor.darkGray)
}
private var backgroundColor: Color {
colorScheme == .light ? .white : Color(.systemGray6)
}
private var shadowColor: Color {
colorScheme == .light ? .black.opacity(0.1) : .black.opacity(0.3)
}
// MARK: - Helper Methods
private func calculateScale(isPortrait: Bool, isPhone: Bool) -> CGFloat {
let baseScale: CGFloat = isPhone ? 0.95 : 1.0
return isPortrait ? baseScale : baseScale * 0.92
}
private func targetSize(for containerSize: CGSize, ignoreSafeArea: Bool = false) -> CGSize {
let targetAspect: CGFloat = {
switch ryujinx.aspectRatio {
case .fixed4x3: return 4.0 / 3.0
case .fixed16x9: return 16.0 / 9.0
case .fixed16x10: return 16.0 / 10.0
case .fixed21x9: return 21.0 / 9.0
case .fixed32x9: return 32.0 / 10.0
case .stretched: return 16.0 / 9.0
}
}()
let safeArea: UIEdgeInsets = ignoreSafeArea ? .zero : UIApplication.shared.windows.first?.safeAreaInsets ?? .zero
let adjustedContainer = CGSize(
width: containerSize.width - safeArea.left - safeArea.right,
height: containerSize.height - safeArea.top - safeArea.bottom
)
let containerAspect = adjustedContainer.width / adjustedContainer.height
if containerAspect > targetAspect {
let height = adjustedContainer.height
let width = height * targetAspect
return CGSize(width: width, height: height)
} else {
let width = adjustedContainer.width
let height = width / targetAspect
return CGSize(width: width, height: height)
}
}
private func isScreenAspectRatio(_ targetWidth: CGFloat, _ targetHeight: CGFloat, tolerance: CGFloat = 0.05) -> Bool {
let screenSize = UIScreen.main.bounds.size
let actualRatio = screenSize.width / screenSize.height
let targetRatio = targetWidth / targetHeight
return abs(actualRatio - targetRatio) < tolerance
}
}

View File

@ -99,7 +99,7 @@ class MeloMTKView: MTKView {
let disabled = UserDefaults.standard.bool(forKey: "disableTouch") let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
guard !disabled else { return } guard !disabled else { return }
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9) setAspectRatio(Ryujinx.shared.aspectRatio)
for touch in touches { for touch in touches {
let location = touch.location(in: self) let location = touch.location(in: self)
@ -153,7 +153,7 @@ class MeloMTKView: MTKView {
let disabled = UserDefaults.standard.bool(forKey: "disableTouch") let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
guard !disabled else { return } guard !disabled else { return }
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9) setAspectRatio(Ryujinx.shared.aspectRatio)
for touch in touches { for touch in touches {
if ignoredTouches.contains(touch) { if ignoredTouches.contains(touch) {

View File

@ -21,14 +21,13 @@ struct MetalView: UIViewRepresentable {
fatalError("[Swift] Error: MTKView's layer is not a CAMetalLayer") fatalError("[Swift] Error: MTKView's layer is not a CAMetalLayer")
} }
metalLayer.device = MTLCreateSystemDefaultDevice() notnil(metalLayer.device) ? () : (metalLayer.device = MTLCreateSystemDefaultDevice())
let layerPtr = Unmanaged.passUnretained(metalLayer).toOpaque() let layerPtr = Unmanaged.passUnretained(metalLayer).toOpaque()
set_native_window(layerPtr) set_native_window(layerPtr)
Ryujinx.shared.emulationUIView = view Ryujinx.shared.emulationUIView = view
Ryujinx.shared.metalLayer = metalLayer Ryujinx.shared.metalLayer = metalLayer
return view return view
@ -51,5 +50,7 @@ struct MetalView: UIViewRepresentable {
func updateUIView(_ uiView: UIView, context: Context) { func updateUIView(_ uiView: UIView, context: Context) {
// nothin // nothin
print(context)
} }
} }

View File

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

View File

@ -52,6 +52,9 @@ struct ContentView: View {
@AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = true @AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = true
@AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = false @AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = false
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false @AppStorage("ignoreJIT") var ignoreJIT: Bool = false
@AppStorage("DUAL_MAPPED_JIT") var dualMapped: Bool = false
@AppStorage("DUAL_MAPPED_JIT_edit") var dualMappededit: Bool = false
// Loading Animation // Loading Animation
@AppStorage("showlogsloading") var showlogsloading: Bool = true @AppStorage("showlogsloading") var showlogsloading: Bool = true
@ -79,9 +82,13 @@ struct ContentView: View {
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) if #available(iOS 19, *) {
setenv("HAS_TXM", ProcessInfo.processInfo.hasTXM ? "1" : "0", 1)
} else {
setenv("HAS_TXM", "0", 1)
}
// print(SDL_CONTROLLER_BUTTON_LEFTSTICK.rawValue) _settings = State(initialValue: defaultSettings)
initializeSDL() initializeSDL()
} }
@ -132,7 +139,6 @@ struct ContentView: View {
JITPopover() { JITPopover() {
ryujinx.jitenabled = false ryujinx.jitenabled = false
} }
// .interactiveDismissDisabled()
} }
} }
@ -149,15 +155,10 @@ struct ContentView: View {
let _ = loadSettings() let _ = loadSettings()
isLoading = true isLoading = true
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in refreshControllersList()
refreshControllersList()
}
UserDefaults.standard.set(false, forKey: "lockInApp") UserDefaults.standard.set(false, forKey: "lockInApp")
// print(MTLHud.shared.isEnabled)
initControllerObservers() initControllerObservers()
Air.play(AnyView( Air.play(AnyView(
@ -166,7 +167,6 @@ struct ContentView: View {
refreshControllersList() refreshControllersList()
ryujinx.addGames() ryujinx.addGames()
checkJitStatus() checkJitStatus()
@ -287,7 +287,6 @@ struct ContentView: View {
queue: .main queue: .main
) { notification in ) { notification in
if let controller = notification.object as? GCController { if let controller = notification.object as? GCController {
// print("Controller connected: \(controller.productCategory)")
nativeControllers[controller] = .init(controller) nativeControllers[controller] = .init(controller)
refreshControllersList() refreshControllersList()
} }
@ -299,7 +298,8 @@ struct ContentView: View {
queue: .main queue: .main
) { notification in ) { notification in
if let controller = notification.object as? GCController { if let controller = notification.object as? GCController {
// print("Controller disconnected: \(controller.productCategory)") currentControllers = []
controllersList = []
nativeControllers[controller]?.cleanup() nativeControllers[controller]?.cleanup()
nativeControllers[controller] = nil nativeControllers[controller] = nil
refreshControllersList() refreshControllersList()
@ -316,6 +316,9 @@ struct ContentView: View {
} }
private func refreshControllersList() { private func refreshControllersList() {
currentControllers = []
controllersList = []
controllersList = ryujinx.getConnectedControllers() controllersList = ryujinx.getConnectedControllers()
if let onscreen = controllersList.first(where: { $0.name == ryujinx.virtualController.controllername }) { if let onscreen = controllersList.first(where: { $0.name == ryujinx.virtualController.controllername }) {
@ -324,11 +327,11 @@ struct ContentView: View {
controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) }) controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) })
controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") } controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") }
currentControllers = []
if controllersList.count == 1 { if controllersList.count == 1 {
currentControllers.append(controllersList[0]) if !ProcessInfo.processInfo.isiOSAppOnMac {
currentControllers.append(controllersList[0])
}
} 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 }) {
@ -398,6 +401,12 @@ struct ContentView: View {
if syncqsubmits { if syncqsubmits {
setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "1", 1) setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "1", 1)
} }
if dualMapped {
setenv("DUAL_MAPPED_JIT", "1", 1)
} else {
setenv("DUAL_MAPPED_JIT", "0", 1)
}
} }
private func setMoltenVKSettings() { private func setMoltenVKSettings() {
@ -411,7 +420,11 @@ struct ContentView: View {
if jitStreamerEB { if jitStreamerEB {
jitStreamerEB = false // byee jitstreamer eb jitStreamerEB = false // byee jitstreamer eb
} }
print("Has TXM? \(ProcessInfo.processInfo.hasTXM)")
if #available(iOS 19, *), !dualMappededit {
dualMapped = !ProcessInfo.processInfo.isiOSAppOnMac
dualMappededit = true
}
if !ryujinx.jitenabled { if !ryujinx.jitenabled {
if useTrollStore { if useTrollStore {
@ -421,12 +434,7 @@ struct ContentView: View {
} else if jitStreamerEB { } else if jitStreamerEB {
enableJITEB() enableJITEB()
} else { } else {
if !allocateTest(), checkDebugged() { // nothing
loop_heartbeat()
sleep(5)
let cool = String(cString: attach(getpid())!)
print(cool)
}
} }
} }
} }
@ -435,8 +443,6 @@ struct ContentView: View {
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true), if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
components.host == "game" { components.host == "game" {
refreshControllersList()
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value { if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
game = ryujinx.games.first(where: { $0.titleId == text }) game = ryujinx.games.first(where: { $0.titleId == text })
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value { } else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {

View File

@ -96,9 +96,15 @@ struct GameInfoSheet: View {
.navigationTitle(game.titleName) .navigationTitle(game.titleName)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .topBarTrailing) {
Button("Dismiss") { if #available(iOS 26, *) {
presentationMode.wrappedValue.dismiss() Button(role: .close) {
presentationMode.wrappedValue.dismiss()
}
} else {
Button("Dismiss") {
presentationMode.wrappedValue.dismiss()
}
} }
} }
} }

View File

@ -27,13 +27,21 @@ struct GameLibraryView: View {
@State var isSelectingGameFile = false @State var isSelectingGameFile = false
@State var isViewingGameInfo: Bool = false @State var isViewingGameInfo: Bool = false
@State var gamePerGameSettings: Game? @State var gamePerGameSettings: Game?
@State var gameController: Game?
var isShowingPerGameSettings: Binding<Bool> { var isShowingPerGameSettings: Binding<Bool> {
Binding<Bool> { Binding<Bool> {
gamePerGameSettings != nil gamePerGameSettings != nil
} set: { value in } set: { value in
!value ? gamePerGameSettings = nil : () !value ? gamePerGameSettings = nil : ()
} }
}
var isShowingGameController: Binding<Bool> {
Binding<Bool> {
gameController != nil
} set: { value in
!value ? gameController = nil : ()
}
} }
@State var isSelectingGameUpdate: Bool = false @State var isSelectingGameUpdate: Bool = false
@State var isSelectingGameDLC: Bool = false @State var isSelectingGameDLC: Bool = false
@ -70,28 +78,38 @@ struct GameLibraryView: View {
var body: some View { var body: some View {
iOSNav { iOSNav {
ZStack { Group {
// Background color // Game list
Color(UIColor.systemBackground) if Ryujinx.shared.games.isEmpty {
.ignoresSafeArea() EmptyGameLibraryView(isSelectingGameFile: $isSelectingGameFile)
} else {
VStack(spacing: 0) { gameListView
// Header with stats .animation(.easeInOut(duration: 0.3), value: searchText)
if !Ryujinx.shared.games.isEmpty { }
GameLibraryHeader( }
totalGames: Ryujinx.shared.games.count, .navigationItemBottomPalette {
recentGames: realRecentGames.count, if !Ryujinx.shared.games.isEmpty {
firmwareVersion: firmwareversion GameLibraryHeader(
) totalGames: Ryujinx.shared.games.count,
} recentGames: realRecentGames.count,
firmwareVersion: firmwareversion
// Game list )
if Ryujinx.shared.games.isEmpty { .overlay(Group {
EmptyGameLibraryView(isSelectingGameFile: $isSelectingGameFile) if ryujinx.jitenabled {
} else { VStack {
gameListView HStack {
.animation(.easeInOut(duration: 0.3), value: searchText) Spacer()
} Circle()
.frame(width: 12, height: 12)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.foregroundColor(Color.green)
.padding()
}
Spacer()
}
}
})
} }
} }
.navigationTitle("Game Library") .navigationTitle("Game Library")
@ -156,27 +174,11 @@ struct GameLibraryView: View {
} }
} }
} }
.overlay(Group {
if ryujinx.jitenabled {
VStack {
HStack {
Spacer()
Circle()
.frame(width: 12, height: 12)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.foregroundColor(Color.green)
.padding()
}
Spacer()
}
}
})
.onChange(of: startemu) { game in .onChange(of: startemu) { game in
guard let game else { return } guard let game else { return }
addToRecentGames(game) addToRecentGames(game)
} }
// .searchable(text: $searchText, placement: .toolbar, prompt: "Search games or developers") .searchable(text: $searchText, placement: .toolbar, prompt: "Search games or developers")
.onChange(of: searchText) { _ in .onChange(of: searchText) { _ in
isSearching = !searchText.isEmpty isSearching = !searchText.isEmpty
} }
@ -213,6 +215,9 @@ struct GameLibraryView: View {
.sheet(isPresented: isShowingPerGameSettings) { .sheet(isPresented: isShowingPerGameSettings) {
PerGameSettingsView(titleId: gamePerGameSettings!.titleId) PerGameSettingsView(titleId: gamePerGameSettings!.titleId)
} }
.fullScreenCover(isPresented: isShowingGameController) {
ControllerView(isEditing: isShowingGameController, gameId: gameController?.titleId)
}
.sheet(isPresented: Binding( .sheet(isPresented: Binding(
get: { isViewingGameInfo && gameInfo != nil }, get: { isViewingGameInfo && gameInfo != nil },
set: { newValue in set: { newValue in
@ -233,7 +238,7 @@ struct GameLibraryView: View {
private var gameListView: some View { private var gameListView: some View {
ScrollView { ScrollView {
LazyVStack(spacing: 0) { LazyVStack(alignment: .leading, spacing: 0) {
if !isSearching && !realRecentGames.isEmpty { if !isSearching && !realRecentGames.isEmpty {
// Recent Games Section // Recent Games Section
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
@ -273,42 +278,27 @@ struct GameLibraryView: View {
.foregroundColor(.primary) .foregroundColor(.primary)
.padding(.horizontal) .padding(.horizontal)
.padding(.top) .padding(.top)
ForEach(filteredGames) { game in
GameListRow(
game: game,
startemu: $startemu,
games: games,
isViewingGameInfo: $isViewingGameInfo,
isSelectingGameUpdate: $isSelectingGameUpdate,
isSelectingGameDLC: $isSelectingGameDLC,
gameRequirements: $gameRequirements,
gameInfo: $gameInfo,
perGameSettings: $gamePerGameSettings
)
.padding(.horizontal)
.padding(.vertical, 8)
}
} }
} }
} else {
ForEach(filteredGames) { game in
GameListRow(
game: game,
startemu: $startemu,
games: games,
isViewingGameInfo: $isViewingGameInfo,
isSelectingGameUpdate: $isSelectingGameUpdate,
isSelectingGameDLC: $isSelectingGameDLC,
gameRequirements: $gameRequirements,
gameInfo: $gameInfo,
perGameSettings: $gamePerGameSettings
)
.padding(.horizontal)
.padding(.vertical, 8)
}
} }
ForEach(filteredGames) { game in
GameListRow(
game: game,
startemu: $startemu,
games: games,
isViewingGameInfo: $isViewingGameInfo,
isSelectingGameUpdate: $isSelectingGameUpdate,
isSelectingGameDLC: $isSelectingGameDLC,
gameRequirements: $gameRequirements,
gameInfo: $gameInfo,
isShowingGameController: $gameController,
perGameSettings: $gamePerGameSettings
)
.padding(.horizontal)
.padding(.vertical, 8)
}
Spacer(minLength: 50) Spacer(minLength: 50)
} }
} }
@ -502,6 +492,12 @@ struct GameLibraryView: View {
} label: { } label: {
Label("\(game.titleName) Settings", systemImage: "gear") Label("\(game.titleName) Settings", systemImage: "gear")
} }
Button {
gameController = game
} label: {
Label("Controller Layout", systemImage: "formfitting.gamecontroller")
}
} }
Section { Section {
@ -527,6 +523,12 @@ struct GameLibraryView: View {
Label("Remove from Recents", systemImage: "trash") Label("Remove from Recents", systemImage: "trash")
} }
Button(role: .destructive) {
Ryujinx.clearShaderCache(game.titleId)
} label: {
Label("Clear Shader Cache", systemImage: "trash")
}
if #available(iOS 15, *) { if #available(iOS 15, *) {
Button(role: .destructive) { Button(role: .destructive) {
deleteGame(game: game) deleteGame(game: game)
@ -629,7 +631,7 @@ struct GameLibraryHeader: View {
// Stats cards // Stats cards
StatCard( StatCard(
icon: "gamecontroller.fill", icon: "gamecontroller.fill",
title: "Total Games", title: "Games",
value: "\(totalGames)", value: "\(totalGames)",
color: .blue color: .blue
) )
@ -649,8 +651,7 @@ struct GameLibraryHeader: View {
) )
} }
.padding(.horizontal) .padding(.horizontal)
.padding(.top, 8) .padding(.bottom, 8)
.padding(.bottom, 4)
} }
} }
@ -659,7 +660,9 @@ struct StatCard: View {
let title: String let title: String
let value: String let value: String
let color: Color let color: Color
@Namespace var statCardIdNamespace
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
HStack { HStack {
@ -675,11 +678,24 @@ struct StatCard: View {
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(10) .padding(10)
.background(color.opacity(0.1)) .apply {
.cornerRadius(10) if #available(iOS 26, *) {
$0
.glassEffect(.regular.tint(color.opacity(0.05)), in: RoundedRectangle(cornerRadius: 16))
.glassEffectID("StatCardID", in: statCardIdNamespace)
} else {
$0
.background(color.opacity(0.1))
.cornerRadius(10)
}
}
} }
} }
extension View {
func apply<V: View>(@ViewBuilder _ block: (Self) -> V) -> V { block(self) }
}
// MARK: - Game Card View // MARK: - Game Card View
struct GameCardView: View { struct GameCardView: View {
let game: Game let game: Game
@ -797,6 +813,7 @@ struct GameListRow: View {
@Binding var isSelectingGameDLC: Bool @Binding var isSelectingGameDLC: Bool
@Binding var gameRequirements: [GameRequirements] @Binding var gameRequirements: [GameRequirements]
@Binding var gameInfo: Game? @Binding var gameInfo: Game?
@Binding var isShowingGameController: Game?
@StateObject private var settingsManager = PerGameSettingsManager.shared @StateObject private var settingsManager = PerGameSettingsManager.shared
@Binding var perGameSettings: Game? @Binding var perGameSettings: Game?
@State var gametoDelete: Game? @State var gametoDelete: Game?
@ -855,20 +872,11 @@ struct GameListRow: View {
} }
} }
} }
if $settingsManager.config.wrappedValue.contains(where: { $0.key == game.titleId }) {
Image(systemName: "gearshape.circle")
.resizable()
.aspectRatio(contentMode: .fill)
.foregroundStyle(.blue)
.frame(width: 20, height: 20)
}
Spacer() Spacer()
VStack(alignment: .leading) { VStack(alignment: .trailing) {
// Compatibility badges // Compatibility badges
HStack {
if let gameReq = gameRequirements.first(where: { $0.game_id == game.titleId }) { if let gameReq = gameRequirements.first(where: { $0.game_id == game.titleId }) {
let totalMemory = ProcessInfo.processInfo.physicalMemory let totalMemory = ProcessInfo.processInfo.physicalMemory
@ -902,6 +910,15 @@ struct GameListRow: View {
.layoutPriority(1) .layoutPriority(1)
} }
} }
HStack {
if $settingsManager.config.wrappedValue.contains(where: { $0.key == game.titleId }) {
Image(systemName: "gearshape.circle")
.resizable()
.aspectRatio(contentMode: .fill)
.foregroundStyle(.blue)
.frame(width: 20, height: 20)
}
// Play button // Play button
Image(systemName: "play.circle.fill") Image(systemName: "play.circle.fill")
@ -915,57 +932,6 @@ struct GameListRow: View {
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
.contextMenu {
Section {
Button {
startemu = game
} label: {
Label("Play Now", systemImage: "play.fill")
}
Button {
gameInfo = game
isViewingGameInfo.toggle()
if game.titleName.lowercased() == "portal" || game.titleName.lowercased() == "portal 2" {
gamepo = true
}
} label: {
Label("Game Info", systemImage: "info.circle")
}
Button {
perGameSettings = game
} label: {
Label("\(game.titleName) Settings", systemImage: "gear")
}
}
Section {
Button {
gameInfo = game
isSelectingGameUpdate.toggle()
} label: {
Label("Update Manager", systemImage: "arrow.up.circle")
}
Button {
gameInfo = game
isSelectingGameDLC.toggle()
} label: {
Label("DLC Manager", systemImage: "plus.circle")
}
}
Section {
Button(role: .destructive) {
gametoDelete = game
showGameDeleteConfirmation.toggle()
} label: {
Label("Delete", systemImage: "trash")
}
}
}
.swipeActions(edge: .trailing) { .swipeActions(edge: .trailing) {
Button(role: .destructive) { Button(role: .destructive) {
gametoDelete = game gametoDelete = game
@ -1002,6 +968,71 @@ struct GameListRow: View {
} }
.listRowInsets(EdgeInsets()) .listRowInsets(EdgeInsets())
.wow(colorScheme) .wow(colorScheme)
.contextMenu {
Section {
Button {
startemu = game
} label: {
Label("Play Now", systemImage: "play.fill")
}
Button {
gameInfo = game
isViewingGameInfo.toggle()
if game.titleName.lowercased() == "portal" || game.titleName.lowercased() == "portal 2" {
gamepo = true
}
} label: {
Label("Game Info", systemImage: "info.circle")
}
Button {
perGameSettings = game
} label: {
Label("\(game.titleName) Settings", systemImage: "gear")
}
// isShowingGameController
Button {
isShowingGameController = game
} label: {
Label("Controller Layout", systemImage: "formfitting.gamecontroller")
}
}
Section {
Button {
gameInfo = game
isSelectingGameUpdate.toggle()
} label: {
Label("Update Manager", systemImage: "arrow.up.circle")
}
Button {
gameInfo = game
isSelectingGameDLC.toggle()
} label: {
Label("DLC Manager", systemImage: "plus.circle")
}
}
Section {
Button(role: .destructive) {
Ryujinx.clearShaderCache(game.titleId)
} label: {
Label("Clear Shader Cache", systemImage: "trash")
}
Button(role: .destructive) {
gametoDelete = game
showGameDeleteConfirmation.toggle()
} label: {
Label("Delete", systemImage: "trash")
}
}
}
} else { } else {
Button(action: { Button(action: {
startemu = game startemu = game
@ -1114,6 +1145,12 @@ struct GameListRow: View {
} label: { } label: {
Label("Game Info", systemImage: "info.circle") Label("Game Info", systemImage: "info.circle")
} }
Button {
isShowingGameController = game
} label: {
Label("Controller Layout", systemImage: "formfitting.gamecontroller")
}
} }
Section { Section {
@ -1133,6 +1170,12 @@ struct GameListRow: View {
} }
Section { Section {
Button(role: .destructive) {
Ryujinx.clearShaderCache(game.titleId)
} label: {
Label("Clear Shader Cache", systemImage: "trash")
}
Button { Button {
gametoDelete = game gametoDelete = game
showGameDeleteConfirmation.toggle() showGameDeleteConfirmation.toggle()
@ -1238,30 +1281,10 @@ func pullGameCompatibility(completion: @escaping (Result<[GameRequirements], Err
extension View { extension View {
func wow(_ colorScheme: ColorScheme) -> some View { func wow(_ colorScheme: ColorScheme) -> some View {
if #available(iOS 26.0, *) { self
return self .background(
.glassEffect(Glass.regular, in: RoundedRectangle(cornerRadius: 12)
RoundedRectangle(cornerRadius: 12) .fill(Color(uiColor: .secondarySystemGroupedBackground))
) )
} else {
return self
.background(
RoundedRectangle(cornerRadius: 12)
.fill(colorScheme == .dark ? Color(.systemGray6) : Color(.systemGray6).opacity(0.5))
)
}
} }
} }
extension View {
@available(iOS, introduced: 14.0, deprecated: 19.0, message: "")
func glassEffect(_ style: Glass, in shape: some Shape) -> some View {
return self
}
}
@available(iOS, introduced: 14.0, deprecated: 19.0, message: "")
struct Glass: Hashable {
static var regular = Glass()
}

View File

@ -168,50 +168,44 @@ struct PerGameSettingsView: View {
var body: some View { var body: some View {
iOSNav { iOSNav {
ZStack { // Settings content
Color(UIColor.systemBackground) ScrollView {
.ignoresSafeArea() VStack(spacing: 24) {
switch selectedCategory {
VStack(spacing: 0) { case .graphics:
ScrollView(.horizontal, showsIndicators: false) { graphicsSettings
HStack(spacing: 12) { .padding(.top)
ForEach(PerSettingsCategory.allCases, id: \.id) { category in case .system:
CategoryButton( systemSettings
title: category.rawValue, .padding(.top)
icon: category.icon, case .advanced:
isSelected: selectedCategory == category advancedSettings
) { .padding(.top)
selectedCategory = category
}
}
}
.padding(.horizontal)
.padding(.vertical, 8)
}
Divider()
// Settings content
ScrollView {
VStack(spacing: 24) {
switch selectedCategory {
case .graphics:
graphicsSettings
.padding(.top)
case .system:
systemSettings
.padding(.top)
case .advanced:
advancedSettings
.padding(.top)
}
Spacer(minLength: 50)
}
.padding(.bottom)
} }
.scrollDismissesKeyboardIfAvailable()
Spacer(minLength: 50)
}
.padding(.bottom)
}
.background(Color(uiColor: .systemGroupedBackground))
.scrollDismissesKeyboardIfAvailable()
.navigationItemBottomPalette {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(PerSettingsCategory.allCases, id: \.id) { category in
CategoryButton(
title: category.rawValue,
icon: category.icon,
isSelected: selectedCategory == category
) {
selectedCategory = category
}
}
}
.defaultScrollAnchorIsAvailable(.center)
.padding(.horizontal)
.padding(.vertical, 8)
} }
} }
.navigationTitle("Settings") .navigationTitle("Settings")

View File

@ -232,6 +232,7 @@ struct SettingsViewNew: View {
@AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = false @AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = false
@AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = false @AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = false
@AppStorage("DUAL_MAPPED_JIT") var dualMapped: Bool = false
@AppStorage("performacehud") var performacehud: Bool = false @AppStorage("performacehud") var performacehud: Bool = false
@ -260,20 +261,18 @@ struct SettingsViewNew: View {
@AppStorage("disableTouch") var disableTouch = false @AppStorage("disableTouch") var disableTouch = false
@AppStorage("disableTouch") var blackScreen = false
@AppStorage("location-enabled") var locationenabled: Bool = false @AppStorage("location-enabled") var locationenabled: Bool = false
@AppStorage("runOnMainThread") var runOnMainThread = false @AppStorage("runOnMainThread") var runOnMainThread = false
@AppStorage("oldSettingsUI") var oldSettingsUI = false @AppStorage("oldSettingsUI") var oldSettingsUI = false
@AppCodableStorage("toggleButtons") var toggleButtons = ToggleButtonsState()
let totalMemory = ProcessInfo.processInfo.physicalMemory let totalMemory = ProcessInfo.processInfo.physicalMemory
@AppStorage("lockInApp") var restartApp = false @AppStorage("lockInApp") var restartApp = false
@AppStorage("OldView") var oldView = true
@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
@ -375,12 +374,26 @@ struct SettingsViewNew: View {
color: .blue color: .blue
) )
InfoCard( let versionPart = ProcessInfo.processInfo.operatingSystemVersionString.replacingOccurrences(of: "Version ", with: "")
title: "System",
value: "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)", let parts = versionPart.components(separatedBy: " (Build ")
icon: "applelogo", if parts.count == 2 {
color: .gray let version = parts[0]
) let build = parts[1].replacingOccurrences(of: ")", with: "")
InfoCard(
title: "System",
value: "\(ProcessInfo.processInfo.isiOSAppOnMac ? "macOS" : UIDevice.current.systemName) \(version) (\(build))",
icon: "applelogo",
color: .gray
)
} else {
InfoCard(
title: "System",
value: "\(ProcessInfo.processInfo.isiOSAppOnMac ? "macOS" : UIDevice.current.systemName) \(UIDevice.current.systemVersion)",
icon: "applelogo",
color: .gray
)
}
InfoCard( InfoCard(
title: "Increased Memory Limit", title: "Increased Memory Limit",
@ -456,58 +469,32 @@ struct SettingsViewNew: View {
var iOSSettings: some View { var iOSSettings: some View {
iOSNav { iOSNav {
ZStack { // Settings content
// Background color ScrollView {
Color(UIColor.systemBackground) VStack(spacing: 24) {
.ignoresSafeArea() deviceInfoCard
VStack(spacing: 0) {
// Category selector
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(SettingsCategory.allCases, id: \.id) { category in
CategoryButton(
title: category.rawValue,
icon: category.icon,
isSelected: selectedCategory == category
) {
selectedCategory = category
}
}
}
.padding(.horizontal) .padding(.horizontal)
.padding(.vertical, 8) .padding(.top)
switch selectedCategory {
case .graphics:
graphicsSettings
case .input:
inputSettings
case .system:
systemSettings
case .advanced:
advancedSettings
case .misc:
miscSettings
} }
Divider() Spacer(minLength: 50)
// Settings content
ScrollView {
VStack(spacing: 24) {
deviceInfoCard
.padding(.horizontal)
.padding(.top)
switch selectedCategory {
case .graphics:
graphicsSettings
case .input:
inputSettings
case .system:
systemSettings
case .advanced:
advancedSettings
case .misc:
miscSettings
}
Spacer(minLength: 50)
}
.padding(.bottom)
}
.scrollDismissesKeyboardIfAvailable()
} }
.padding(.bottom)
} }
.scrollDismissesKeyboardIfAvailable()
.background(Color(uiColor: .systemGroupedBackground))
.navigationTitle("Settings") .navigationTitle("Settings")
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.large)
// .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic)) // .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic))
@ -520,6 +507,25 @@ struct SettingsViewNew: View {
settingsManager.saveSettings() settingsManager.saveSettings()
} }
} }
.navigationItemBottomPalette {
// Category selector
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(SettingsCategory.allCases, id: \.id) { category in
CategoryButton(
title: category.rawValue,
icon: category.icon,
isSelected: selectedCategory == category
) {
selectedCategory = category
}
}
}
.defaultScrollAnchorIsAvailable(.center)
.padding(.horizontal)
.padding(.vertical, 8)
}
}
} }
} }
@ -551,6 +557,16 @@ struct SettingsViewNew: View {
.font(.subheadline) .font(.subheadline)
.foregroundColor(.secondary) .foregroundColor(.secondary)
if ProcessInfo.processInfo.isiOSAppOnMac {
Text("macOS \(ProcessInfo.processInfo.operatingSystemVersionString)")
.font(.subheadline.weight(.medium))
.foregroundColor(.secondary)
Text("·")
.font(.subheadline)
.foregroundColor(.secondary)
}
Text("Version \(appVersion)") Text("Version \(appVersion)")
.font(.subheadline) .font(.subheadline)
.foregroundColor(.secondary) .foregroundColor(.secondary)
@ -755,6 +771,11 @@ struct SettingsViewNew: View {
// Aspect ratio card // Aspect ratio card
SettingsCard { SettingsCard {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
SettingsToggle(isOn: $oldView, icon: "rectangle.on.rectangle.dashed", label: "Old Display UI")
Divider()
labelWithIcon("Aspect Ratio", iconName: "rectangle.expand.vertical") labelWithIcon("Aspect Ratio", iconName: "rectangle.expand.vertical")
.font(.headline) .font(.headline)
@ -825,16 +846,6 @@ struct SettingsViewNew: View {
Divider() Divider()
SettingsToggle(isOn: $swapBandA, icon: "rectangle.2.swap", label: "Swap Face Buttons (Physical Controller)") SettingsToggle(isOn: $swapBandA, icon: "rectangle.2.swap", label: "Swap Face Buttons (Physical Controller)")
Divider()
DisclosureGroup("Toggle Buttons") {
SettingsToggle(isOn: $toggleButtons.toggle1, icon: "circle.grid.cross.right.filled", label: "Toggle A")
SettingsToggle(isOn: $toggleButtons.toggle2, icon: "circle.grid.cross.down.filled", label: "Toggle B")
SettingsToggle(isOn: $toggleButtons.toggle3, icon: "circle.grid.cross.up.filled", label: "Toggle X")
SettingsToggle(isOn: $toggleButtons.toggle4, icon: "circle.grid.cross.left.filled", label: "Toggle Y")
}
.padding(.vertical, 6)
} }
} }
@ -1242,6 +1253,7 @@ struct SettingsViewNew: View {
SettingsSection(title: "Miscellaneous Options") { SettingsSection(title: "Miscellaneous Options") {
SettingsCard { SettingsCard {
VStack(spacing: 4) { VStack(spacing: 4) {
if UIDevice.current.userInterfaceIdiom == .pad { if UIDevice.current.userInterfaceIdiom == .pad {
SettingsToggle(isOn: $toggleGreen, icon: "arrow.clockwise", label: "Toggle Color Green when \"ON\"") SettingsToggle(isOn: $toggleGreen, icon: "arrow.clockwise", label: "Toggle Color Green when \"ON\"")
@ -1254,11 +1266,6 @@ struct SettingsViewNew: View {
Divider() Divider()
if colorScheme == .light {
SettingsToggle(isOn: $blackScreen, icon: "iphone.slash", label: "Black Screen when using AirPlay")
Divider()
}
Button { Button {
showAppIconSwitcher = true showAppIconSwitcher = true
@ -1354,20 +1361,25 @@ struct SettingsViewNew: View {
Divider() Divider()
SettingsToggle(isOn: $dualMapped, icon: "light.strip.2", label: "Dual Mapped JIT")
Divider()
SettingsToggle(isOn: $checkForUpdate, icon: "square.and.arrow.down", label: "Check for Updates") SettingsToggle(isOn: $checkForUpdate, icon: "square.and.arrow.down", label: "Check for Updates")
if ryujinx.firmwareversion != "0" { Divider()
Divider()
Button { Button {
Ryujinx.shared.removeFirmware() Ryujinx.clearShaderCache()
} label: { } label: {
HStack { HStack {
Text("Remove Firmware") Image(systemName: "trash")
.foregroundColor(.blue) .foregroundColor(.blue)
Spacer() Text("Clear All Shader Cache")
} .foregroundColor(.primary)
.padding(.vertical, 8) Spacer()
} }
.padding(.vertical, 8)
} }
} }
} }
@ -1498,12 +1510,23 @@ struct CategoryButton: View {
} }
.foregroundColor(isSelected ? .blue : .secondary) .foregroundColor(isSelected ? .blue : .secondary)
.frame(width: 70, height: 56) .frame(width: 70, height: 56)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(isSelected ? Color.blue.opacity(0.15) : Color.clear)
)
.animation(.bouncy(duration: 0.3), value: isSelected) .animation(.bouncy(duration: 0.3), value: isSelected)
} }
.apply {
if #available(iOS 26, *) {
if isSelected {
$0.glassEffect(.regular.tint(Color.blue.opacity(0.15)), in: RoundedRectangle(cornerRadius: 16))
} else {
$0
}
} else {
$0.background(
RoundedRectangle(cornerRadius: 12)
.fill(isSelected ? Color.blue.opacity(0.15) : Color.clear)
)
}
}
} }
} }
@ -1542,7 +1565,7 @@ struct SettingsCard<Content: View>: View {
.padding() .padding()
.background( .background(
RoundedRectangle(cornerRadius: 12) RoundedRectangle(cornerRadius: 12)
.fill(colorScheme == .dark ? Color(.systemGray6) : Color.white) .fill(Color(.secondarySystemGroupedBackground))
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2) .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
) )
.padding(.horizontal) .padding(.horizontal)
@ -1667,3 +1690,14 @@ extension View {
} }
} }
// this code is used to enable the keyboard to be dismissed when scrolling if available on iOS 16+
extension View {
@ViewBuilder
func defaultScrollAnchorIsAvailable(_ anchor: UnitPoint?) -> some View {
if #available(iOS 17.0, *) {
self.defaultScrollAnchor(anchor)
} else {
self
}
}
}

View File

@ -41,7 +41,11 @@ struct MeloNXApp: App {
@AppStorage("autoJIT") var autoJIT = false @AppStorage("autoJIT") var autoJIT = false
@State var fourgbiPad = false @State var fourgbiPad = false
@State var ios19 = false
@AppStorage("4GB iPad") var ignores = false @AppStorage("4GB iPad") var ignores = false
@AppStorage("iOS19") var ignores19 = false
@AppStorage("DUAL_MAPPED_JIT") var dualMapped: Bool = false
@AppStorage("DUAL_MAPPED_JIT_edit") var dualMappededit: Bool = false
// String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000) // String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000)
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
@ -77,10 +81,19 @@ struct MeloNXApp: App {
withAnimation(.easeOut) { withAnimation(.easeOut) {
finishedStorage = newValue finishedStorage = newValue
} }
if #available(iOS 19, *), newValue {
dualMapped = !ProcessInfo.processInfo.isiOSAppOnMac
dualMappededit = true
}
} }
} }
} }
.onAppear() { .onAppear() {
if #available(iOS 19, *), ProcessInfo.processInfo.hasTXM, !ignores19 {
ios19 = true
}
if UIDevice.current.userInterfaceIdiom == .pad && !ignores { if UIDevice.current.userInterfaceIdiom == .pad && !ignores {
print((Double(ProcessInfo.processInfo.physicalMemory) / 1_000_000_000)) print((Double(ProcessInfo.processInfo.physicalMemory) / 1_000_000_000))
if round(Double(ProcessInfo.processInfo.physicalMemory) / 1_000_000_000) <= 4 { if round(Double(ProcessInfo.processInfo.physicalMemory) / 1_000_000_000) <= 4 {
@ -148,3 +161,4 @@ func changeAppUI(_ string: String) -> String? {
guard let data = Data(base64Encoded: string) else { return nil } guard let data = Data(base64Encoded: string) else { return nil }
return String(data: data, encoding: .utf8) return String(data: data, encoding: .utf8)
} }

View File

@ -0,0 +1,615 @@
{
"sourceLanguage" : "en",
"strings" : {
"" : {
},
"-" : {
"comment" : "A button that decreases the size of a button.",
"isCommentAutoGenerated" : true
},
"·" : {
"comment" : "A separator displayed between two lines of text.",
"isCommentAutoGenerated" : true
},
"**%@** | %@" : {
"comment" : "A heading displaying the name of a game, followed by its title ID.",
"isCommentAutoGenerated" : true,
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "**%1$@** | %2$@"
}
}
}
},
"**File Type**" : {
"comment" : "A label displayed above the file type of a game.",
"isCommentAutoGenerated" : true
},
"**Game Size**" : {
"comment" : "A label displayed above the size of a game file.",
"isCommentAutoGenerated" : true
},
"**Game URL**" : {
"comment" : "A label displayed above the URL of a game file.",
"isCommentAutoGenerated" : true
},
"**Title ID**" : {
"comment" : "A button that copies the title ID to the clipboard.",
"isCommentAutoGenerated" : true
},
"**Version**" : {
"comment" : "A label displayed before the version number of a game.",
"isCommentAutoGenerated" : true
},
"%.1fx" : {
"comment" : "A placeholder text for the slider value.",
"isCommentAutoGenerated" : true
},
"%.2fx" : {
"comment" : "The current resolution scale value is displayed in blue text.",
"isCommentAutoGenerated" : true
},
"%@" : {
"comment" : "A label displaying the current FPS and memory usage in low power mode.",
"isCommentAutoGenerated" : true
},
"%@ DLCs" : {
},
"%@ RAM" : {
"comment" : "A subheading displaying the amount of RAM available on a device.",
"isCommentAutoGenerated" : true
},
"%@ Settings" : {
"comment" : "A button that allows the user to access the settings for a specific game.",
"isCommentAutoGenerated" : true
},
"%@ Updates" : {
},
"%lld icons" : {
"comment" : "A label displaying the number of icons created by a developer.",
"isCommentAutoGenerated" : true
},
"%llu bytes" : {
"comment" : "A label displaying the size of a file.",
"isCommentAutoGenerated" : true
},
"•" : {
"comment" : "A separator between the developer name and the version number.",
"isCommentAutoGenerated" : true
},
"+" : {
"comment" : "A button that increases the scale of a button when pressed.",
"isCommentAutoGenerated" : true
},
"0.1x" : {
"comment" : "The word \"low\" is used here to mean \"minimum\".",
"isCommentAutoGenerated" : true
},
"0GB" : {
"comment" : "A placeholder text displayed when the system requirements of a game cannot be determined.",
"isCommentAutoGenerated" : true
},
"3.0x" : {
"comment" : "The \"1x\" text is a placeholder.",
"isCommentAutoGenerated" : true
},
"16x" : {
"comment" : "The \"16x\" text in the slider.",
"isCommentAutoGenerated" : true
},
"About" : {
},
"Add Controller" : {
"comment" : "A button that allows adding a controller to the list.",
"isCommentAutoGenerated" : true
},
"Add DLC" : {
"comment" : "A button that adds game DLCs.",
"isCommentAutoGenerated" : true
},
"Add Game" : {
"comment" : "A button that adds a game to the library.",
"isCommentAutoGenerated" : true
},
"Add ROM files to get started with your gaming experience" : {
"comment" : "A call-to-action displayed below the message that no games were found.",
"isCommentAutoGenerated" : true
},
"Add Update" : {
"comment" : "A button that adds a new game update.",
"isCommentAutoGenerated" : true
},
"Additional Arguments" : {
"comment" : "A heading displayed above a text field used to enter additional arguments for the virtual machine.",
"isCommentAutoGenerated" : true
},
"Adjust the internal Anisotropic filtering. Higher values improve texture quality at angles but may reduce performance. Default at 0 lets game decide." : {
"comment" : "A description displayed in an alert when the user taps the \"Max Anisotropic Filtering\" setting.",
"isCommentAutoGenerated" : true
},
"Adjust the internal rendering resolution. Higher values improve visuals but may reduce performance." : {
"comment" : "A description displayed in an alert when the user taps the \"Info\" icon next to \"Resolution Scale\".",
"isCommentAutoGenerated" : true
},
"Adjust the On-Screen Controller size." : {
"comment" : "A description displayed when the user taps the \"Info\" icon next to the \"Scale\" label in the \"On-Screen Controller\" card.",
"isCommentAutoGenerated" : true
},
"Adjust the On-Screen Controller transparency." : {
"comment" : "A description displayed in an alert dialog box.",
"isCommentAutoGenerated" : true
},
"Always show Joystick Background" : {
"comment" : "A label displayed above a toggle button that controls whether the joystick background is always shown.",
"isCommentAutoGenerated" : true
},
"App Icon Switcher" : {
"comment" : "A",
"isCommentAutoGenerated" : true
},
"Applets" : {
"comment" : "A menu that allows users to launch specific apps on their Switch.",
"isCommentAutoGenerated" : true
},
"Are you sure you want to delete %@?" : {
},
"Are you sure you want to delete this game?" : {
},
"Available Layouts" : {
},
"Button Scale: %@" : {
"comment" : "A label displaying the current scale of a button.",
"isCommentAutoGenerated" : true
},
"Cancel" : {
"comment" : "The title of the alert that confirms deleting a custom layout.",
"isCommentAutoGenerated" : true
},
"Changelog:" : {
"comment" : "A heading displayed above a list of changes in the latest update.",
"isCommentAutoGenerated" : true
},
"Choose App Icon" : {
"comment" : "The title displayed in the navigation bar above the list of app icons.",
"isCommentAutoGenerated" : true
},
"Clear All Shader Cache" : {
"comment" : "A",
"isCommentAutoGenerated" : true
},
"Clear Shader Cache" : {
"comment" : "A button that deletes the shader cache for a game.",
"isCommentAutoGenerated" : true
},
"Close" : {
"comment" : "The label for the button to close the update sheet.",
"isCommentAutoGenerated" : true
},
"Continue" : {
"comment" : "A button that dismisses the alert.",
"isCommentAutoGenerated" : true
},
"Controller Configuration" : {
"comment" : "A heading displayed above the controller configuration options.",
"isCommentAutoGenerated" : true
},
"Controller Layout" : {
},
"Controller Selection" : {
"comment" : "A heading displayed above a list of available controllers.",
"isCommentAutoGenerated" : true
},
"Copy Layout" : {
"comment" : "A title displayed in the navigation bar of the view.",
"isCommentAutoGenerated" : true
},
"Copy Layout From..." : {
"comment" : "A button label to copy a layout from another game.",
"isCommentAutoGenerated" : true
},
"Copy Title ID" : {
"comment" : "A button label to copy a text to the clipboard.",
"isCommentAutoGenerated" : true
},
"CPU Configuration" : {
"comment" : "A heading displayed above the CPU configuration options.",
"isCommentAutoGenerated" : true
},
"Current Game" : {
"comment" : "A heading displayed above the information about the current game.",
"isCommentAutoGenerated" : true
},
"Custom Layout" : {
"comment" : "A caption displayed next to a checkmark icon.",
"isCommentAutoGenerated" : true
},
"Default Layout" : {
"comment" : "A button label for copying the default layout.",
"isCommentAutoGenerated" : true
},
"Delete" : {
"comment" : "The destructive button in an alert that deletes a custom layout.",
"isCommentAutoGenerated" : true
},
"Delete Custom Layout" : {
"comment" : "A button label to delete a custom layout.",
"isCommentAutoGenerated" : true
},
"Delete Game" : {
"comment" : "A destructive button that deletes a game from the library.",
"isCommentAutoGenerated" : true
},
"Dismiss" : {
"comment" : "A button that dismisses the game info sheet.",
"isCommentAutoGenerated" : true
},
"DLC Manager" : {
"comment" : "A button that displays a sheet for managing downloadable content for a game.",
"isCommentAutoGenerated" : true
},
"Done" : {
"comment" : "A button that toggles between editing mode and non-editing mode.",
"isCommentAutoGenerated" : true
},
"Download Now" : {
"comment" : "The action button to download the app update.",
"isCommentAutoGenerated" : true
},
"Edit" : {
"comment" : "A button that toggles between editing mode and non-editing mode.",
"isCommentAutoGenerated" : true
},
"Exit (Unstable)" : {
"comment" : "A destructive action displayed in a menu.",
"isCommentAutoGenerated" : true
},
"Finish Setup" : {
},
"Game" : {
},
"Game Info" : {
"comment" : "A button that displays information about a game.",
"isCommentAutoGenerated" : true
},
"Game Library" : {
"comment" : "The title displayed in the navigation bar for the game library view.",
"isCommentAutoGenerated" : true
},
"Game: %@" : {
"comment" : "A label displaying the identifier of the game being played.",
"isCommentAutoGenerated" : true
},
"Games" : {
"comment" : "A tab item that displays a list of games.",
"isCommentAutoGenerated" : true
},
"Hide" : {
"comment" : "A button that hides the edit controls when pressed.",
"isCommentAutoGenerated" : true
},
"Hide ABXY / Arrow Buttons" : {
"comment" : "A label displayed above a toggle button that controls whether the joystick buttons are hidden or not.",
"isCommentAutoGenerated" : true
},
"Hide Button" : {
"comment" : "A label displayed above a toggle switch that controls whether a button is hidden or not.",
"isCommentAutoGenerated" : true
},
"Hide Joystick" : {
"comment" : "A button to hide the joystick.",
"isCommentAutoGenerated" : true
},
"Home Menu (Broken)" : {
"comment" : "A button that launches the currently Broken Home Menu applet."
},
"Info" : {
},
"Information" : {
"comment" : "A heading displayed above a list of information about a game.",
"isCommentAutoGenerated" : true
},
"Install Firmware" : {
"comment" : "A button that allows users to install the latest firmware for their Switch.",
"isCommentAutoGenerated" : true
},
"JIT (Just-In-Time) compilation allows MeloNX to run code at as fast as possible by translating it dynamically. This is necessary for running this emulator." : {
"comment" : "A description of what JIT is.",
"isCommentAutoGenerated" : true
},
"JIT Enabled" : {
"comment" : "A heading displayed above the status of JIT and the amount of RAM.",
"isCommentAutoGenerated" : true
},
"JIT Not Acquired" : {
"comment" : "A heading displayed above the status of JIT and the amount of RAM.",
"isCommentAutoGenerated" : true
},
"Joystick" : {
"comment" : "A label displayed inside the circle of a joystick.",
"isCommentAutoGenerated" : true
},
"Joystick Scale: %@" : {
"comment" : "A label displaying the current scale of a joystick.",
"isCommentAutoGenerated" : true
},
"Larger" : {
"comment" : "A placeholder text that indicates the minimum value of the slider.",
"isCommentAutoGenerated" : true
},
"Launch ${gameName}" : {
},
"Launch Game" : {
"comment" : "Title of the intent.",
"isCommentAutoGenerated" : true
},
"Launch Mii Maker" : {
"comment" : "A button that launches the Mii Maker applet.",
"isCommentAutoGenerated" : true
},
"Launches the Selected Game." : {
"comment" : "A short description of the intent.",
"isCommentAutoGenerated" : true
},
"Layout Actions" : {
"comment" : "A heading displayed above a list of actions related to layouts.",
"isCommentAutoGenerated" : true
},
"Layout Options" : {
"comment" : "A button that allows the user to customize the layout of the controller.",
"isCommentAutoGenerated" : true
},
"Less Transparent" : {
},
"Library" : {
"comment" : "A label displayed above the list of games in the library.",
"isCommentAutoGenerated" : true
},
"Loading %@" : {
"comment" : "A label displayed while the game is loading.",
"isCommentAutoGenerated" : true
},
"macOS %@" : {
"comment" : "A subheading displaying the macOS version running on the user's device.",
"isCommentAutoGenerated" : true
},
"Make Button Toggle" : {
"comment" : "A checkbox displayed next to a text label.",
"isCommentAutoGenerated" : true
},
"Max Anisotropic Filtering" : {
"comment" : "A title displayed above a description of a setting.",
"isCommentAutoGenerated" : true
},
"Memory Manager Mode" : {
"comment" : "A label displayed above a picker view that allows the user to choose the memory manager mode.",
"isCommentAutoGenerated" : true
},
"More Transparent" : {
"comment" : "A heading displayed above the opacity of a UI element.",
"isCommentAutoGenerated" : true
},
"No controllers selected (Keyboard will be used)" : {
"comment" : "A text indicating that no controllers are selected.",
"isCommentAutoGenerated" : true
},
"No DLCs Found" : {
"comment" : "A message displayed when no DLCs are found.",
"isCommentAutoGenerated" : true
},
"No Games Found" : {
"comment" : "A message displayed when a user has no games in their library.",
"isCommentAutoGenerated" : true
},
"No Updates Found" : {
"comment" : "The title of a view that displays a list of items.",
"isCommentAutoGenerated" : true
},
"Off" : {
"comment" : "A caption displayed above the slider for max anisotropic filtering.",
"isCommentAutoGenerated" : true
},
"OK" : {
"comment" : "The button label to dismiss the alert.",
"isCommentAutoGenerated" : true
},
"ON" : {
"comment" : "The text displayed when the toggle is on or off.",
"isCommentAutoGenerated" : true
},
"On-Screen Controller" : {
"comment" : "A heading displayed above the scale settings for the on-screen controller.",
"isCommentAutoGenerated" : true
},
"On-Screen Controller Opacity" : {
"comment" : "A heading displayed above the description of the opacity setting for the on-screen controller.",
"isCommentAutoGenerated" : true
},
"On-Screen Controller Scale" : {
"comment" : "A title displayed above a message about the scale of the on-screen controller.",
"isCommentAutoGenerated" : true
},
"Open Game" : {
"comment" : "A button that allows the user to open a game file.",
"isCommentAutoGenerated" : true
},
"Options" : {
"comment" : "A button that displays a menu with options for the game library.",
"isCommentAutoGenerated" : true
},
"Pause" : {
"comment" : "A label displayed above a button that pauses or plays the emulation.",
"isCommentAutoGenerated" : true
},
"Play" : {
"comment" : "A label displayed above a button that pauses or plays the emulation.",
"isCommentAutoGenerated" : true
},
"Play Now" : {
"comment" : "A button that allows the user to launch a game.",
"isCommentAutoGenerated" : true
},
"Player %lld: %@" : {
"comment" : "A label displaying the name of a controller and its position in the list of controllers.",
"isCommentAutoGenerated" : true,
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Player %1$lld: %2$@"
}
}
}
},
"Recent Games" : {
"comment" : "A heading displayed above a list of recently played games.",
"isCommentAutoGenerated" : true
},
"Remove from Recents" : {
"comment" : "A button that deletes a game from the user's library.",
"isCommentAutoGenerated" : true
},
"Reset" : {
"comment" : "A button that resets all settings to their default values.",
"isCommentAutoGenerated" : true
},
"Reset Current" : {
"comment" : "A button that resets the layout to its default state.",
"isCommentAutoGenerated" : true
},
"Reset Selected" : {
"comment" : "A button that resets the layout settings for a specific button when pressed.",
"isCommentAutoGenerated" : true
},
"Reset to Default Layout" : {
"comment" : "A button label to reset a layout to its default state.",
"isCommentAutoGenerated" : true
},
"Resolution Scale" : {
"comment" : "The title of an alert displayed when the user taps the \"i\" icon next to the \"Resolution Scale\" label.",
"isCommentAutoGenerated" : true
},
"Save to Photos" : {
"comment" : "A button that saves an image to the user's Photos library.",
"isCommentAutoGenerated" : true
},
"Separate arguments with commas" : {
"comment" : "A text field that allows the user to specify additional arguments to pass to the game.",
"isCommentAutoGenerated" : true
},
"Set up your Nintendo Switch emulation environment by importing keys and firmware." : {
"comment" : "A description displayed below the welcome message.",
"isCommentAutoGenerated" : true
},
"Settings" : {
"comment" : "The title displayed in the navigation bar of the settings view.",
"isCommentAutoGenerated" : true
},
"Setup" : {
},
"Show MeloNX Folder" : {
"comment" : "A button that opens the folder containing the game files on macOS.",
"isCommentAutoGenerated" : true
},
"Show Setup Screen" : {
"comment" : "A button label that shows a setup screen.",
"isCommentAutoGenerated" : true
},
"Skip" : {
"comment" : "A button that allows the user to skip the setup process.",
"isCommentAutoGenerated" : true
},
"Skip Setup?" : {
"comment" : "A prompt displayed when the user wants to skip setup.",
"isCommentAutoGenerated" : true
},
"Smaller" : {
"comment" : "A label displayed above the slider that controls the size of the on-screen controller.",
"isCommentAutoGenerated" : true
},
"Tap the + button to add game DLCs." : {
"comment" : "A message displayed when no game DLCs are found.",
"isCommentAutoGenerated" : true
},
"Tap the + button to add game updates." : {
"comment" : "A description displayed when no game updates are found.",
"isCommentAutoGenerated" : true
},
"The cake is a lie" : {
"comment" : "A placeholder text for a card in the settings menu.",
"isCommentAutoGenerated" : true
},
"This will delete the custom layout for this game and revert to the default layout." : {
"comment" : "A message displayed when the user confirms to delete the custom layout.",
"isCommentAutoGenerated" : true
},
"Unsupported Device" : {
"comment" : "An alert that appears on iPad devices with less than 4 GB of memory.",
"isCommentAutoGenerated" : true
},
"Update Manager" : {
"comment" : "A button that displays a sheet for updating a game.",
"isCommentAutoGenerated" : true
},
"Using Default" : {
"comment" : "A caption displayed when the user has not customized a layout for a game.",
"isCommentAutoGenerated" : true
},
"v%@" : {
"comment" : "A text indicating the version of a game.",
"isCommentAutoGenerated" : true
},
"Version %@" : {
"comment" : "A label displaying the version of the app.",
"isCommentAutoGenerated" : true
},
"Version %@ Available!" : {
"comment" : "The title of the sheet that appears when a new version of the app is available.",
"isCommentAutoGenerated" : true
},
"Version %@ is available. You are currently on Version %@." : {
"comment" : "A message that informs users a new version of the app is available, along with the current version and the version that is available.",
"isCommentAutoGenerated" : true,
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Version %1$@ is available. You are currently on Version %2$@."
}
}
}
},
"Waiting for JIT" : {
"comment" : "A heading displayed when MeloNX is waiting for JIT compilation to complete.",
"isCommentAutoGenerated" : true
},
"Welcome to MeloNX" : {
"comment" : "A welcome message displayed on the initial setup screen.",
"isCommentAutoGenerated" : true
},
"wow" : {
"comment" : "A placeholder text used for demonstration purposes.",
"isCommentAutoGenerated" : true
},
"Your Device is an iPad with %@ of memory, MeloNX has issues with those devices" : {
"comment" : "A message displayed when the user's iPad has less than 4 GB of memory.",
"isCommentAutoGenerated" : true
}
},
"version" : "1.1"
}

View File

@ -0,0 +1,28 @@
//
// BreakJIT.h
// BreakpointJIT
//
// Created by Stossy11 on 09/07/2025.
//
#ifndef BreakGetJITMapping_h
#define BreakGetJITMapping_h
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Function with inline assembly for JIT mapping
* @param bytes Size parameter for mapping
* @return char* pointer result
*/
__attribute__((noinline, optnone, naked)) char* BreakGetJITMapping(size_t bytes);
#ifdef __cplusplus
}
#endif
#endif /* BreakGetJITMapping_h */

View File

@ -1,330 +0,0 @@
#if 0
#elif defined(__arm64__) && __arm64__
// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)
#ifndef STOSJIT_SWIFT_H
#define STOSJIT_SWIFT_H
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
#if !defined(__has_include)
# define __has_include(x) 0
#endif
#if !defined(__has_attribute)
# define __has_attribute(x) 0
#endif
#if !defined(__has_feature)
# define __has_feature(x) 0
#endif
#if !defined(__has_warning)
# define __has_warning(x) 0
#endif
#if __has_include(<swift/objc-prologue.h>)
# include <swift/objc-prologue.h>
#endif
#pragma clang diagnostic ignored "-Wauto-import"
#if defined(__OBJC__)
#include <Foundation/Foundation.h>
#endif
#if defined(__cplusplus)
#include <cstdint>
#include <cstddef>
#include <cstdbool>
#include <cstring>
#include <stdlib.h>
#include <new>
#include <type_traits>
#else
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#endif
#if defined(__cplusplus)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module"
#if defined(__arm64e__) && __has_include(<ptrauth.h>)
# include <ptrauth.h>
#else
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wreserved-macro-identifier"
# ifndef __ptrauth_swift_value_witness_function_pointer
# define __ptrauth_swift_value_witness_function_pointer(x)
# endif
# ifndef __ptrauth_swift_class_method_pointer
# define __ptrauth_swift_class_method_pointer(x)
# endif
#pragma clang diagnostic pop
#endif
#pragma clang diagnostic pop
#endif
#if !defined(SWIFT_TYPEDEFS)
# define SWIFT_TYPEDEFS 1
# if __has_include(<uchar.h>)
# include <uchar.h>
# elif !defined(__cplusplus)
typedef uint_least16_t char16_t;
typedef uint_least32_t char32_t;
# endif
typedef float swift_float2 __attribute__((__ext_vector_type__(2)));
typedef float swift_float3 __attribute__((__ext_vector_type__(3)));
typedef float swift_float4 __attribute__((__ext_vector_type__(4)));
typedef double swift_double2 __attribute__((__ext_vector_type__(2)));
typedef double swift_double3 __attribute__((__ext_vector_type__(3)));
typedef double swift_double4 __attribute__((__ext_vector_type__(4)));
typedef int swift_int2 __attribute__((__ext_vector_type__(2)));
typedef int swift_int3 __attribute__((__ext_vector_type__(3)));
typedef int swift_int4 __attribute__((__ext_vector_type__(4)));
typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2)));
typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3)));
typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4)));
#endif
#if !defined(SWIFT_PASTE)
# define SWIFT_PASTE_HELPER(x, y) x##y
# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y)
#endif
#if !defined(SWIFT_METATYPE)
# define SWIFT_METATYPE(X) Class
#endif
#if !defined(SWIFT_CLASS_PROPERTY)
# if __has_feature(objc_class_property)
# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__
# else
# define SWIFT_CLASS_PROPERTY(...)
# endif
#endif
#if !defined(SWIFT_RUNTIME_NAME)
# if __has_attribute(objc_runtime_name)
# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X)))
# else
# define SWIFT_RUNTIME_NAME(X)
# endif
#endif
#if !defined(SWIFT_COMPILE_NAME)
# if __has_attribute(swift_name)
# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X)))
# else
# define SWIFT_COMPILE_NAME(X)
# endif
#endif
#if !defined(SWIFT_METHOD_FAMILY)
# if __has_attribute(objc_method_family)
# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X)))
# else
# define SWIFT_METHOD_FAMILY(X)
# endif
#endif
#if !defined(SWIFT_NOESCAPE)
# if __has_attribute(noescape)
# define SWIFT_NOESCAPE __attribute__((noescape))
# else
# define SWIFT_NOESCAPE
# endif
#endif
#if !defined(SWIFT_RELEASES_ARGUMENT)
# if __has_attribute(ns_consumed)
# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed))
# else
# define SWIFT_RELEASES_ARGUMENT
# endif
#endif
#if !defined(SWIFT_WARN_UNUSED_RESULT)
# if __has_attribute(warn_unused_result)
# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
# else
# define SWIFT_WARN_UNUSED_RESULT
# endif
#endif
#if !defined(SWIFT_NORETURN)
# if __has_attribute(noreturn)
# define SWIFT_NORETURN __attribute__((noreturn))
# else
# define SWIFT_NORETURN
# endif
#endif
#if !defined(SWIFT_CLASS_EXTRA)
# define SWIFT_CLASS_EXTRA
#endif
#if !defined(SWIFT_PROTOCOL_EXTRA)
# define SWIFT_PROTOCOL_EXTRA
#endif
#if !defined(SWIFT_ENUM_EXTRA)
# define SWIFT_ENUM_EXTRA
#endif
#if !defined(SWIFT_CLASS)
# if __has_attribute(objc_subclassing_restricted)
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA
# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# else
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# endif
#endif
#if !defined(SWIFT_RESILIENT_CLASS)
# if __has_attribute(objc_class_stub)
# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub))
# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME)
# else
# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME)
# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME)
# endif
#endif
#if !defined(SWIFT_PROTOCOL)
# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
#endif
#if !defined(SWIFT_EXTENSION)
# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__)
#endif
#if !defined(OBJC_DESIGNATED_INITIALIZER)
# if __has_attribute(objc_designated_initializer)
# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
# else
# define OBJC_DESIGNATED_INITIALIZER
# endif
#endif
#if !defined(SWIFT_ENUM_ATTR)
# if __has_attribute(enum_extensibility)
# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility)))
# else
# define SWIFT_ENUM_ATTR(_extensibility)
# endif
#endif
#if !defined(SWIFT_ENUM)
# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
# if __has_feature(generalized_swift_name)
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
# else
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
# endif
#endif
#if !defined(SWIFT_UNAVAILABLE)
# define SWIFT_UNAVAILABLE __attribute__((unavailable))
#endif
#if !defined(SWIFT_UNAVAILABLE_MSG)
# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg)))
#endif
#if !defined(SWIFT_AVAILABILITY)
# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__)))
#endif
#if !defined(SWIFT_WEAK_IMPORT)
# define SWIFT_WEAK_IMPORT __attribute__((weak_import))
#endif
#if !defined(SWIFT_DEPRECATED)
# define SWIFT_DEPRECATED __attribute__((deprecated))
#endif
#if !defined(SWIFT_DEPRECATED_MSG)
# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__)))
#endif
#if !defined(SWIFT_DEPRECATED_OBJC)
# if __has_feature(attribute_diagnose_if_objc)
# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning")))
# else
# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg)
# endif
#endif
#if defined(__OBJC__)
#if !defined(IBSegueAction)
# define IBSegueAction
#endif
#endif
#if !defined(SWIFT_EXTERN)
# if defined(__cplusplus)
# define SWIFT_EXTERN extern "C"
# else
# define SWIFT_EXTERN extern
# endif
#endif
#if !defined(SWIFT_CALL)
# define SWIFT_CALL __attribute__((swiftcall))
#endif
#if !defined(SWIFT_INDIRECT_RESULT)
# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result))
#endif
#if !defined(SWIFT_CONTEXT)
# define SWIFT_CONTEXT __attribute__((swift_context))
#endif
#if !defined(SWIFT_ERROR_RESULT)
# define SWIFT_ERROR_RESULT __attribute__((swift_error_result))
#endif
#if defined(__cplusplus)
# define SWIFT_NOEXCEPT noexcept
#else
# define SWIFT_NOEXCEPT
#endif
#if !defined(SWIFT_C_INLINE_THUNK)
# if __has_attribute(always_inline)
# if __has_attribute(nodebug)
# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug))
# else
# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline))
# endif
# else
# define SWIFT_C_INLINE_THUNK inline
# endif
#endif
#if defined(_WIN32)
#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport)
#endif
#else
#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
# define SWIFT_IMPORT_STDLIB_SYMBOL
#endif
#endif
#if defined(__OBJC__)
#if __has_feature(objc_modules)
#if __has_warning("-Watimport-in-framework-header")
#pragma clang diagnostic ignored "-Watimport-in-framework-header"
#endif
#endif
#endif
#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
#pragma clang diagnostic ignored "-Wduplicate-method-arg"
#if __has_warning("-Wpragma-clang-attribute")
# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
#endif
#pragma clang diagnostic ignored "-Wunknown-pragmas"
#pragma clang diagnostic ignored "-Wnullability"
#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
#pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
#if __has_attribute(external_source_symbol)
# pragma push_macro("any")
# undef any
# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="StosJIT",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
# pragma pop_macro("any")
#endif
#if defined(__OBJC__)
SWIFT_EXTERN char * _Nullable attach(int32_t pid) SWIFT_NOEXCEPT SWIFT_WARN_UNUSED_RESULT;
SWIFT_EXTERN char * _Nullable debugattachanddetachApp(char * _Nonnull bundleId) SWIFT_NOEXCEPT SWIFT_WARN_UNUSED_RESULT;
SWIFT_EXTERN void detach(void) SWIFT_NOEXCEPT;
SWIFT_EXTERN void loop_heartbeat(void) SWIFT_NOEXCEPT;
SWIFT_EXTERN BOOL writeZeroToMemory(uint64_t addr, int32_t length) SWIFT_NOEXCEPT SWIFT_WARN_UNUSED_RESULT;
#endif
#if __has_attribute(external_source_symbol)
# pragma clang attribute pop
#endif
#if defined(__cplusplus)
#endif
#pragma clang diagnostic pop
#endif
#else
#error unsupported Swift architecture
#endif

View File

@ -1,19 +0,0 @@
//
// StosJIT.h
// StosJIT
//
// Created by Stossy11 on 10/05/2025.
//
#import <Foundation/Foundation.h>
#import <StosJIT/idevice.h>
//! Project version number for StosJIT.
FOUNDATION_EXPORT double StosJITVersionNumber;
//! Project version string for StosJIT.
FOUNDATION_EXPORT const unsigned char StosJITVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <StosJIT/PublicHeader.h>

View File

@ -1,205 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>files</key>
<dict>
<key>.DS_Store</key>
<data>
7Mfr8shT4pXWBr/plN+uNkIabdM=
</data>
<key>Headers/StosJIT-Swift.h</key>
<data>
h9vaTwhC6FlnyKmIkaxLQGlFd1g=
</data>
<key>Headers/StosJIT.h</key>
<data>
ggHr5wlLNIIPydwUL9Vxm6abxjo=
</data>
<key>Headers/idevice.h</key>
<data>
mHDz7368FsBID56/epJ2NgIkha4=
</data>
<key>Headers/plist.h</key>
<data>
bL/f0MQDpLfvIcI1zxPwMuJ/PfI=
</data>
<key>Info.plist</key>
<data>
ZTTwPKlta/gjXAr1HIHmyAxeU4E=
</data>
<key>Modules/StosJIT.swiftmodule/Project/arm64-apple-ios.swiftsourceinfo</key>
<data>
nihJghwM5m7kxkQD7UvrWyHkLy8=
</data>
<key>Modules/StosJIT.swiftmodule/arm64-apple-ios.abi.json</key>
<data>
gcwBsH4BgyFY4sVtNt+/xOKS3vY=
</data>
<key>Modules/StosJIT.swiftmodule/arm64-apple-ios.swiftdoc</key>
<data>
YPtkDrAuBiPPEp4ZdRdBVlFXnRM=
</data>
<key>Modules/StosJIT.swiftmodule/arm64-apple-ios.swiftmodule</key>
<data>
9cIInnjJzJFtY+CZm2iNo5qL3MQ=
</data>
<key>Modules/module.modulemap</key>
<data>
cnpvYzvLIwWcxkQodj5uLbHkyRk=
</data>
</dict>
<key>files2</key>
<dict>
<key>Headers/StosJIT-Swift.h</key>
<dict>
<key>hash2</key>
<data>
1obIr4IjMvtcyNyYIV/Nh/5wahcA1cFjc4n4XVlNt2I=
</data>
</dict>
<key>Headers/StosJIT.h</key>
<dict>
<key>hash2</key>
<data>
yY9KyrRdOYRdlb7G6wVMU2hogasXMjwV5r8jUIk44ok=
</data>
</dict>
<key>Headers/idevice.h</key>
<dict>
<key>hash2</key>
<data>
zR9/TB9Dnv3uRC8qqGvaQ6c2yyOFUURmrHKLdEiUh/g=
</data>
</dict>
<key>Headers/plist.h</key>
<dict>
<key>hash2</key>
<data>
yFbGsiXBBp91tfsSFtS0Utt2Gpc3MEDFiMVXKG9q1rs=
</data>
</dict>
<key>Modules/StosJIT.swiftmodule/Project/arm64-apple-ios.swiftsourceinfo</key>
<dict>
<key>hash2</key>
<data>
+Ehvco7cQbAaF7zufvBYTiGXFp37Hjym/Pav514sGPk=
</data>
</dict>
<key>Modules/StosJIT.swiftmodule/arm64-apple-ios.abi.json</key>
<dict>
<key>hash2</key>
<data>
Qnesa0n4URGWAopawg9bGx36dUwkYV00BoCJ8LFzlyg=
</data>
</dict>
<key>Modules/StosJIT.swiftmodule/arm64-apple-ios.swiftdoc</key>
<dict>
<key>hash2</key>
<data>
k7F2Xs2hh9iMbK8IE8TMtN6gjQ9kWs30NUKHeupq6VE=
</data>
</dict>
<key>Modules/StosJIT.swiftmodule/arm64-apple-ios.swiftmodule</key>
<dict>
<key>hash2</key>
<data>
gMDYNHcBPCNwZw2A5mEUiCyYAS9VhtQG0z+/WqAUrOQ=
</data>
</dict>
<key>Modules/module.modulemap</key>
<dict>
<key>hash2</key>
<data>
FGwGKs5SNvpCyiIWiOP4eml9m2e3KISmtCJVtNnUnUc=
</data>
</dict>
</dict>
<key>rules</key>
<dict>
<key>^.*</key>
<true/>
<key>^.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^version.plist$</key>
<true/>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^.*</key>
<true/>
<key>^.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^Info\.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^PkgInfo$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^embedded\.provisionprofile$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^version\.plist$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
</dict>
</dict>
</plist>

View File

@ -81,14 +81,14 @@ namespace Ryujinx.Audio.Renderer.Dsp
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static short GetCoefficientAtIndex(ReadOnlySpan<short> coefficients, int index) private static short GetCoefficientAtIndex(ReadOnlySpan<short> coefficients, int index)
{ {
if ((uint)index > (uint)coefficients.Length) if ((uint)index < (uint)coefficients.Length)
{ {
Logger.Error?.Print(LogClass.AudioRenderer, $"Out of bound read for coefficient at index {index}"); return coefficients[index];
return 0;
} }
return coefficients[index]; Logger.Error?.Print(LogClass.AudioRenderer, $"Out of bound read for coefficient at index {index}");
return 0;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@ -11,7 +11,7 @@ using Ryujinx.Audio.Renderer.Server.Sink;
using Ryujinx.Audio.Renderer.Server.Splitter; using Ryujinx.Audio.Renderer.Server.Splitter;
using Ryujinx.Audio.Renderer.Server.Types; using Ryujinx.Audio.Renderer.Server.Types;
using Ryujinx.Audio.Renderer.Server.Upsampler; using Ryujinx.Audio.Renderer.Server.Upsampler;
using Ryujinx.Audio.Renderer.Server.Voice; using Ryujinx.Audio.Renderer.Server.Voice;
using Ryujinx.Audio.Renderer.Utils; using Ryujinx.Audio.Renderer.Utils;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;

View File

@ -2,23 +2,17 @@ using ARMeilleure.Memory;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
using System.Runtime.InteropServices;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
namespace Ryujinx.Cpu.LightningJit.Cache namespace Ryujinx.Cpu.LightningJit.Cache
{ {
class WriteZeroCache : IDisposable class DualMappedNoWxCache : IDisposable
{ {
private const int CodeAlignment = 4; private const int CodeAlignment = 4; // Bytes.
private const int InitialCacheSize = 2 * 1024 * 1024; private const int SharedCacheSize = 512 * 1024 * 1024;
private const int GrowthCacheSize = 2 * 1024 * 1024; private const int LocalCacheSize = 128 * 1024 * 1024;
private const int MaxSharedCacheSize = 512 * 1024 * 1024;
private const int MaxLocalCacheSize = 128 * 1024 * 1024;
[DllImport("StosJIT.framework/StosJIT", EntryPoint = "writeZeroToMemory")]
public static extern bool WriteZeroToMemory(ulong addr, int length);
// 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
// and allow the guest to take the fast path. // and allow the guest to take the fast path.
@ -26,104 +20,24 @@ namespace Ryujinx.Cpu.LightningJit.Cache
private class MemoryCache : IDisposable private class MemoryCache : IDisposable
{ {
private readonly ReservedRegion _region; private readonly DualMappedJitAllocator _allocator;
private readonly CacheMemoryAllocator _cacheAllocator; private readonly CacheMemoryAllocator _cacheAllocator;
public readonly IJitMemoryAllocator Allocator; public DualMappedJitAllocator Allocator => _allocator;
private readonly ulong _maxSize; public IntPtr RwPointer => _allocator.RwPtr;
private ulong _currentSize; public IntPtr RxPointer => _allocator.RxPtr;
private readonly Dictionary<int, HashSet<int>> _reusePages;
private readonly object _reuselock = new object();
public CacheMemoryAllocator CacheAllocator => _cacheAllocator; public CacheMemoryAllocator CacheAllocator => _cacheAllocator;
public IntPtr Pointer => _region.Block.Pointer; public IntPtr Pointer => _allocator.RwPtr;
public ulong CurrentSize => _currentSize;
public ulong MaxSize => _maxSize;
public MemoryCache(IJitMemoryAllocator allocator, ulong maxSize) public MemoryCache(ulong size)
{ {
Allocator = allocator; _allocator = new DualMappedJitAllocator(size);
_maxSize = maxSize; _cacheAllocator = new((int)size);
_currentSize = InitialCacheSize;
_region = new(allocator, maxSize);
_cacheAllocator = new((int)maxSize);
_reusePages = new Dictionary<int, HashSet<int>>();
_region.Block.MapAsRw(0, _currentSize);
_region.ExpandIfNeeded(_currentSize);
WriteZeroToMemory((ulong)_region.Block.Pointer.ToInt64(), (int)_currentSize);
}
public bool TryGetReusablePage(int size, out int offset)
{
lock (_reuselock)
{
if (_reusePages.TryGetValue(size, out var exactOffsets) && exactOffsets.Count > 0)
{
offset = exactOffsets.First();
exactOffsets.Remove(offset);
return true;
}
var largerSizes = _reusePages.Where(kvp => kvp.Key > size && kvp.Value.Count > 0)
.OrderBy(kvp => kvp.Key)
.FirstOrDefault();
if (largerSizes.Value != null && largerSizes.Value.Count > 0)
{
int largerSize = largerSizes.Key;
var largerOffsets = largerSizes.Value;
offset = largerOffsets.First();
largerOffsets.Remove(offset);
int remainingSize = largerSize - size;
if (remainingSize > 0)
{
AddReusablePage(offset + size, remainingSize);
}
return true;
}
offset = -1;
return false;
}
}
public void AddReusablePage(int offset, int size)
{
if (size < (int)MemoryBlock.GetPageSize())
{
return;
}
lock (_reuselock)
{
if (!_reusePages.TryGetValue(size, out var offsets))
{
offsets = new HashSet<int>();
_reusePages[size] = offsets;
}
offsets.Add(offset);
}
} }
public int Allocate(int codeSize) public int Allocate(int codeSize)
{ {
codeSize = AlignCodeSize(codeSize); codeSize = AlignCodeSize(codeSize);
if (codeSize >= (int)MemoryBlock.GetPageSize() &&
(codeSize % (int)MemoryBlock.GetPageSize() == 0) &&
TryGetReusablePage(codeSize, out int reuseOffset))
{
ReprotectAsRw(reuseOffset, codeSize);
return reuseOffset;
}
int allocOffset = _cacheAllocator.Allocate(codeSize); int allocOffset = _cacheAllocator.Allocate(codeSize);
@ -132,63 +46,19 @@ namespace Ryujinx.Cpu.LightningJit.Cache
throw new OutOfMemoryException("JIT Cache exhausted."); throw new OutOfMemoryException("JIT Cache exhausted.");
} }
ulong requiredSize = (ulong)allocOffset + (ulong)codeSize;
if (requiredSize > _currentSize)
{
ulong neededGrowth = requiredSize - _currentSize;
ulong growthIncrements = (neededGrowth + GrowthCacheSize - 1) / GrowthCacheSize;
ulong newSize = _currentSize + (growthIncrements * GrowthCacheSize);
newSize = Math.Min(newSize, _maxSize);
if (newSize <= _currentSize || requiredSize > newSize)
{
throw new OutOfMemoryException("JIT Cache exhausted, cannot grow further.");
}
_region.Block.MapAsRw(_currentSize, newSize - _currentSize);
_region.ExpandIfNeeded(newSize);
WriteZeroToMemory((ulong)(_region.Block.Pointer.ToInt64() + (long)_currentSize), (int)(newSize - _currentSize));
_currentSize = newSize;
}
return allocOffset; return allocOffset;
} }
public void Free(int offset, int size) public void Free(int offset, int size)
{ {
if (size >= (int)MemoryBlock.GetPageSize() && (size % (int)MemoryBlock.GetPageSize() == 0) && _cacheAllocator.Free(offset, size);
(offset % (int)MemoryBlock.GetPageSize() == 0))
{
AddReusablePage(offset, size);
}
else
{
_cacheAllocator.Free(offset, size);
}
} }
public void ReprotectAsRw(int offset, int size) public void SysIcacheInvalidate(int offset, int size)
{ {
Debug.Assert(offset >= 0 && (offset & (int)(MemoryBlock.GetPageSize() - 1)) == 0);
Debug.Assert(size > 0 && (size & (int)(MemoryBlock.GetPageSize() - 1)) == 0);
_region.Block.MapAsRw((ulong)offset, (ulong)size);
}
public void ReprotectAsRx(int offset, int size)
{
Debug.Assert(offset >= 0 && (offset & (int)(MemoryBlock.GetPageSize() - 1)) == 0);
Debug.Assert(size > 0 && (size & (int)(MemoryBlock.GetPageSize() - 1)) == 0);
_region.Block.MapAsRx((ulong)offset, (ulong)size);
if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
{ {
JitSupportDarwin.SysIcacheInvalidate(_region.Block.Pointer + offset, size); JitSupportDarwin.SysIcacheInvalidate(_allocator.RxPtr + offset, size);
} }
else else
{ {
@ -196,14 +66,6 @@ namespace Ryujinx.Cpu.LightningJit.Cache
} }
} }
public void ClearReusePool()
{
lock (_reuselock)
{
_reusePages.Clear();
}
}
private static int AlignCodeSize(int codeSize) private static int AlignCodeSize(int codeSize)
{ {
return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1); return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1);
@ -213,8 +75,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{ {
if (disposing) if (disposing)
{ {
ClearReusePool(); _allocator.Dispose();
_region.Dispose();
_cacheAllocator.Clear(); _cacheAllocator.Clear();
} }
} }
@ -259,12 +120,12 @@ namespace Ryujinx.Cpu.LightningJit.Cache
[ThreadStatic] [ThreadStatic]
private static Dictionary<ulong, ThreadLocalCacheEntry> _threadLocalCache; private static Dictionary<ulong, ThreadLocalCacheEntry> _threadLocalCache;
public WriteZeroCache(IJitMemoryAllocator allocator, IStackWalker stackWalker, Translator translator) public DualMappedNoWxCache(IJitMemoryAllocator allocator, IStackWalker stackWalker, Translator translator)
{ {
_stackWalker = stackWalker; _stackWalker = stackWalker;
_translator = translator; _translator = translator;
_sharedCaches = new List<MemoryCache> { new(allocator, MaxSharedCacheSize) }; _sharedCaches = new List<MemoryCache> { new(SharedCacheSize) };
_localCaches = new List<MemoryCache> { new(allocator, MaxLocalCacheSize) }; _localCaches = new List<MemoryCache> { new(LocalCacheSize) };
_pendingMaps = new Dictionary<ulong, PageAlignedRangeList>(); _pendingMaps = new Dictionary<ulong, PageAlignedRangeList>();
_lock = new(); _lock = new();
} }
@ -275,7 +136,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
if (!_pendingMaps.TryGetValue(cacheKey, out var pendingMap)) if (!_pendingMaps.TryGetValue(cacheKey, out var pendingMap))
{ {
pendingMap = new PageAlignedRangeList( pendingMap = new PageAlignedRangeList(
(offset, size) => _sharedCaches[cacheIndex].ReprotectAsRx(offset, size), (offset, size) => _sharedCaches[cacheIndex].SysIcacheInvalidate(offset, size),
(address, func) => RegisterFunction(address, func)); (address, func) => RegisterFunction(address, func));
_pendingMaps[cacheKey] = pendingMap; _pendingMaps[cacheKey] = pendingMap;
} }
@ -304,15 +165,13 @@ namespace Ryujinx.Cpu.LightningJit.Cache
} }
catch (OutOfMemoryException) catch (OutOfMemoryException)
{ {
// Try next cache
} }
} }
// All existing caches are full, create a new one
lock (_lock) lock (_lock)
{ {
var allocator = _sharedCaches[0].Allocator; var allocator = _sharedCaches[0].Allocator;
_sharedCaches.Add(new(allocator, MaxSharedCacheSize)); _sharedCaches.Add(new(SharedCacheSize));
return (_sharedCaches.Count - 1) << 28 | _sharedCaches[_sharedCaches.Count - 1].Allocate(codeLength); return (_sharedCaches.Count - 1) << 28 | _sharedCaches[_sharedCaches.Count - 1].Allocate(codeLength);
} }
} }
@ -327,14 +186,14 @@ namespace Ryujinx.Cpu.LightningJit.Cache
} }
catch (OutOfMemoryException) catch (OutOfMemoryException)
{ {
// Try next cache
} }
} }
lock (_lock) lock (_lock)
{ {
var allocator = _localCaches[0].Allocator; var allocator = _localCaches[0].Allocator;
_localCaches.Add(new(allocator, MaxLocalCacheSize)); _localCaches.Add(new(LocalCacheSize));
return (_localCaches.Count - 1) << 28 | _localCaches[_localCaches.Count - 1].Allocate(codeLength); return (_localCaches.Count - 1) << 28 | _localCaches[_localCaches.Count - 1].Allocate(codeLength);
} }
} }
@ -360,8 +219,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
MemoryCache cache = _sharedCaches[cacheIndex]; MemoryCache cache = _sharedCaches[cacheIndex];
funcPtr = cache.Pointer + funcOffset; funcPtr = cache.Pointer + funcOffset;
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length)); code.CopyTo(new Span<byte>((void*)funcPtr, code.Length));
funcPtr = cache.RxPointer + funcOffset;
TranslatedFunction function = new(funcPtr, guestSize); TranslatedFunction function = new(funcPtr, guestSize);
@ -396,21 +255,22 @@ namespace Ryujinx.Cpu.LightningJit.Cache
Debug.Assert((funcOffset & ((int)MemoryBlock.GetPageSize() - 1)) == 0); Debug.Assert((funcOffset & ((int)MemoryBlock.GetPageSize() - 1)) == 0);
IntPtr funcPtr1 = _sharedCaches[cacheIndex].Pointer + funcOffset; IntPtr funcPtr1 = _sharedCaches[cacheIndex].Pointer + funcOffset;
code.CopyTo(new Span<byte>((void*)funcPtr1, code.Length)); code.CopyTo(new Span<byte>((void*)funcPtr1, code.Length));
funcPtr1 = _sharedCaches[cacheIndex].RxPointer + funcOffset;
_sharedCaches[cacheIndex].ReprotectAsRx(funcOffset, sizeAligned); _sharedCaches[cacheIndex].SysIcacheInvalidate(funcOffset, sizeAligned);
return funcPtr1; return funcPtr1;
} }
catch (OutOfMemoryException) catch (OutOfMemoryException)
{ {
// Try next cache
} }
} }
var allocator = _sharedCaches[0].Allocator; var allocator = _sharedCaches[0].Allocator;
var newCache = new MemoryCache(allocator, MaxSharedCacheSize); var newCache = new MemoryCache(SharedCacheSize);
_sharedCaches.Add(newCache); _sharedCaches.Add(newCache);
cacheIndex = _sharedCaches.Count - 1; cacheIndex = _sharedCaches.Count - 1;
@ -425,8 +285,9 @@ namespace Ryujinx.Cpu.LightningJit.Cache
IntPtr funcPtr = newCache.Pointer + funcOffset; IntPtr funcPtr = newCache.Pointer + funcOffset;
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length)); code.CopyTo(new Span<byte>((void*)funcPtr, code.Length));
funcPtr = newCache.RxPointer + funcOffset;
newCache.ReprotectAsRx(funcOffset, newSizeAligned); newCache.SysIcacheInvalidate(funcOffset, newSizeAligned);
return funcPtr; return funcPtr;
} }
@ -481,8 +342,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
for (int i = 0; i < _localCaches.Count; i++) for (int i = 0; i < _localCaches.Count; i++)
{ {
cachePointers[i] = _localCaches[i].Pointer; cachePointers[i] = _localCaches[i].RxPointer;
cacheSizes[i] = (int)_localCaches[i].CurrentSize; cacheSizes[i] = LocalCacheSize;
} }
IntPtr[] sharedPointers = new IntPtr[_sharedCaches.Count]; IntPtr[] sharedPointers = new IntPtr[_sharedCaches.Count];
@ -490,19 +351,20 @@ namespace Ryujinx.Cpu.LightningJit.Cache
for (int i = 0; i < _sharedCaches.Count; i++) for (int i = 0; i < _sharedCaches.Count; i++)
{ {
sharedPointers[i] = _sharedCaches[i].Pointer; sharedPointers[i] = _sharedCaches[i].RxPointer;
sharedSizes[i] = (int)_sharedCaches[i].CurrentSize; sharedSizes[i] = SharedCacheSize;
} }
// Iterate over the arrays and pass each element to GetCallStack
IEnumerable<ulong> callStack = null; IEnumerable<ulong> callStack = null;
for (int i = 0; i < _localCaches.Count; i++) for (int i = 0; i < _localCaches.Count; i++)
{ {
callStack = _stackWalker.GetCallStack( callStack = _stackWalker.GetCallStack(
framePointer, framePointer,
cachePointers[i], cachePointers[i], // Passing each individual cachePointer
cacheSizes[i], cacheSizes[i], // Passing each individual cacheSize
sharedPointers[i], sharedPointers[i], // Passing each individual sharedPointer
sharedSizes[i] sharedSizes[i] // Passing each individual sharedSize
); );
} }
@ -510,12 +372,16 @@ namespace Ryujinx.Cpu.LightningJit.Cache
foreach ((ulong address, ThreadLocalCacheEntry entry) in _threadLocalCache) foreach ((ulong address, ThreadLocalCacheEntry entry) in _threadLocalCache)
{ {
// 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.
bool canDelete = !HasInAnyPendingMap(address); bool canDelete = !HasInAnyPendingMap(address);
if (!canDelete) if (!canDelete)
{ {
continue; continue;
} }
// We can only delete if the function is not part of the current thread call stack,
// otherwise we will crash the program when the thread returns to it.
foreach (ulong funcAddress in callStack) foreach (ulong funcAddress in callStack)
{ {
if (funcAddress >= (ulong)entry.FuncPtr && funcAddress < (ulong)entry.FuncPtr + (ulong)entry.Size) if (funcAddress >= (ulong)entry.FuncPtr && funcAddress < (ulong)entry.FuncPtr + (ulong)entry.Size)
@ -541,12 +407,14 @@ namespace Ryujinx.Cpu.LightningJit.Cache
var (cacheIndex, offset) = SplitCacheOffset(entry.Offset); var (cacheIndex, offset) = SplitCacheOffset(entry.Offset);
_localCaches[cacheIndex].Free(offset, sizeAligned); _localCaches[cacheIndex].Free(offset, sizeAligned);
_localCaches[cacheIndex].ReprotectAsRw(offset, sizeAligned);
} }
} }
public void ClearEntireThreadLocalCache() public void ClearEntireThreadLocalCache()
{ {
// Thread is exiting, delete everything.
if (_threadLocalCache == null) if (_threadLocalCache == null)
{ {
return; return;
@ -560,7 +428,6 @@ namespace Ryujinx.Cpu.LightningJit.Cache
var (cacheIndex, offset) = SplitCacheOffset(entry.Offset); var (cacheIndex, offset) = SplitCacheOffset(entry.Offset);
_localCaches[cacheIndex].Free(offset, sizeAligned); _localCaches[cacheIndex].Free(offset, sizeAligned);
_localCaches[cacheIndex].ReprotectAsRw(offset, sizeAligned);
} }
_threadLocalCache.Clear(); _threadLocalCache.Clear();
@ -577,10 +444,11 @@ namespace Ryujinx.Cpu.LightningJit.Cache
IntPtr funcPtr = _localCaches[cacheIndex].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));
funcPtr = _localCaches[cacheIndex].RxPointer + funcOffset;
(_threadLocalCache ??= new()).Add(guestAddress, new(funcOffset, code.Length, funcPtr, cacheIndex)); (_threadLocalCache ??= new()).Add(guestAddress, new(funcOffset, code.Length, funcPtr, cacheIndex));
_localCaches[cacheIndex].ReprotectAsRx(funcOffset, alignedSize); _localCaches[cacheIndex].SysIcacheInvalidate(funcOffset, alignedSize);
return funcPtr; return funcPtr;
} }

View File

@ -5,6 +5,7 @@ using System.Runtime.Versioning;
namespace Ryujinx.Cpu.LightningJit.Cache namespace Ryujinx.Cpu.LightningJit.Cache
{ {
[SupportedOSPlatform("macos")] [SupportedOSPlatform("macos")]
[SupportedOSPlatform("ios")]
static partial class JitSupportDarwin static partial class JitSupportDarwin
{ {
[LibraryImport("libarmeilleure-jitsupport", EntryPoint = "armeilleure_jit_memcpy")] [LibraryImport("libarmeilleure-jitsupport", EntryPoint = "armeilleure_jit_memcpy")]

View File

@ -41,7 +41,7 @@ namespace Ryujinx.Cpu.LightningJit
private readonly ConcurrentQueue<KeyValuePair<ulong, TranslatedFunction>> _oldFuncs; private readonly ConcurrentQueue<KeyValuePair<ulong, TranslatedFunction>> _oldFuncs;
private readonly NoWxCache _noWxCache; private readonly NoWxCache _noWxCache;
private readonly WriteZeroCache _writeZeroCache; private readonly DualMappedNoWxCache _dualMappedCache;
private bool _disposed; private bool _disposed;
internal TranslatorCache<TranslatedFunction> Functions { get; } internal TranslatorCache<TranslatedFunction> Functions { get; }
@ -57,27 +57,14 @@ namespace Ryujinx.Cpu.LightningJit
if (IsNoWxPlatform) if (IsNoWxPlatform)
{ {
if (File.Exists("/System/Library/CoreServices/SystemVersion.plist")) string dualMapped = Environment.GetEnvironmentVariable("DUAL_MAPPED_JIT");
if (dualMapped == "1") //(OperatingSystem.IsIOSVersionAtLeast(19) || OperatingSystem.IsIOSVersionAtLeast(26))
{ {
string content = File.ReadAllText("/System/Library/CoreServices/SystemVersion.plist"); Console.WriteLine($"Dual Mapped JIT enabled.");
if (content.Contains("22E5200s") && content.Contains("18.4") && content.Contains("Beta")) _dualMappedCache = new(new JitMemoryAllocator(), CreateStackWalker(), this);
{ Functions = new TranslatorCache<TranslatedFunction>();
// iOS 18.4db1 (22E5200s) disables traditional JIT (R/X) and needs a debugger to fill to the page to make the executable region a debug map. FunctionTable = new AddressTable<ulong>(for64Bits ? _levels64Bit : _levels32Bit);
// Apple has confirmed that this change will be coming to later iOS releases. Stubs = new TranslatorStubs(FunctionTable, _dualMappedCache);
// Credit to JJTech for figuring out a workaround: https://gist.github.com/JJTech0130/142aee0f7bda9c61a421140d17afbdeb
Console.WriteLine($"User is using iOS 18.4db1 (22E5200s), enabling Debugger Memory Writing");
_writeZeroCache = new(new JitMemoryAllocator(), CreateStackWalker(), this);
Functions = new TranslatorCache<TranslatedFunction>();
FunctionTable = new AddressTable<ulong>(for64Bits ? _levels64Bit : _levels32Bit);
Stubs = new TranslatorStubs(FunctionTable, _writeZeroCache);
}
else
{
_noWxCache = new(new JitMemoryAllocator(), CreateStackWalker(), this);
Functions = new TranslatorCache<TranslatedFunction>();
FunctionTable = new AddressTable<ulong>(for64Bits ? _levels64Bit : _levels32Bit);
Stubs = new TranslatorStubs(FunctionTable, _noWxCache);
}
} }
else else
{ {
@ -125,7 +112,7 @@ namespace Ryujinx.Cpu.LightningJit
NativeInterface.UnregisterThread(); NativeInterface.UnregisterThread();
_noWxCache?.ClearEntireThreadLocalCache(); _noWxCache?.ClearEntireThreadLocalCache();
_writeZeroCache?.ClearEntireThreadLocalCache(); _dualMappedCache?.ClearEntireThreadLocalCache();
} }
internal IntPtr GetOrTranslatePointer(IntPtr framePointer, ulong address, ExecutionMode mode) internal IntPtr GetOrTranslatePointer(IntPtr framePointer, ulong address, ExecutionMode mode)
@ -135,10 +122,10 @@ namespace Ryujinx.Cpu.LightningJit
CompiledFunction func = Compile(address, mode); CompiledFunction func = Compile(address, mode);
return _noWxCache.Map(framePointer, func.Code, address, (ulong)func.GuestCodeLength); return _noWxCache.Map(framePointer, func.Code, address, (ulong)func.GuestCodeLength);
} }
else if (_writeZeroCache != null) else if (_dualMappedCache != null)
{ {
CompiledFunction func = Compile(address, mode); CompiledFunction func = Compile(address, mode);
return _writeZeroCache.Map(framePointer, func.Code, address, (ulong)func.GuestCodeLength); return _dualMappedCache.Map(framePointer, func.Code, address, (ulong)func.GuestCodeLength);
} }
return GetOrTranslate(address, mode).FuncPointer; return GetOrTranslate(address, mode).FuncPointer;
@ -239,9 +226,9 @@ namespace Ryujinx.Cpu.LightningJit
{ {
_noWxCache.Dispose(); _noWxCache.Dispose();
} }
else if (_writeZeroCache != null) else if (_dualMappedCache != null)
{ {
_writeZeroCache.Dispose(); _dualMappedCache.Dispose();
} }
else else
{ {

View File

@ -25,7 +25,7 @@ namespace Ryujinx.Cpu.LightningJit
private readonly AddressTable<ulong> _functionTable; private readonly AddressTable<ulong> _functionTable;
private readonly NoWxCache _noWxCache; private readonly NoWxCache _noWxCache;
private readonly WriteZeroCache _writeZeroCache; private readonly DualMappedNoWxCache _dualMappedCache;
private readonly GetFunctionAddressDelegate _getFunctionAddressRef; private readonly GetFunctionAddressDelegate _getFunctionAddressRef;
private readonly IntPtr _getFunctionAddress; private readonly IntPtr _getFunctionAddress;
@ -101,12 +101,12 @@ namespace Ryujinx.Cpu.LightningJit
/// <param name="functionTable">Function table used to store pointers to the functions that the guest code will call</param> /// <param name="functionTable">Function table used to store pointers to the functions that the guest code will call</param>
/// <param name="writeZeroCache">Cache used on iOS versions that need a debugger to make a debug map</param> /// <param name="writeZeroCache">Cache used on iOS versions that need a debugger to make a debug map</param>
/// <exception cref="ArgumentNullException"><paramref name="translator"/> is null</exception> /// <exception cref="ArgumentNullException"><paramref name="translator"/> is null</exception>
public TranslatorStubs(AddressTable<ulong> functionTable, WriteZeroCache writeZeroCache) public TranslatorStubs(AddressTable<ulong> functionTable, DualMappedNoWxCache dualMappedCache)
{ {
ArgumentNullException.ThrowIfNull(functionTable); ArgumentNullException.ThrowIfNull(functionTable);
_functionTable = functionTable; _functionTable = functionTable;
_writeZeroCache = writeZeroCache; _dualMappedCache = dualMappedCache;
_getFunctionAddressRef = NativeInterface.GetFunctionAddress; _getFunctionAddressRef = NativeInterface.GetFunctionAddress;
_getFunctionAddress = Marshal.GetFunctionPointerForDelegate(_getFunctionAddressRef); _getFunctionAddress = Marshal.GetFunctionPointerForDelegate(_getFunctionAddressRef);
_slowDispatchStub = new(GenerateSlowDispatchStub, isThreadSafe: true); _slowDispatchStub = new(GenerateSlowDispatchStub, isThreadSafe: true);
@ -131,7 +131,7 @@ namespace Ryujinx.Cpu.LightningJit
{ {
if (!_disposed) if (!_disposed)
{ {
if (_noWxCache == null) if (_noWxCache == null || _dualMappedCache == null)
{ {
if (_dispatchStub.IsValueCreated) if (_dispatchStub.IsValueCreated)
{ {
@ -383,9 +383,9 @@ namespace Ryujinx.Cpu.LightningJit
{ {
return _noWxCache.MapPageAligned(code); return _noWxCache.MapPageAligned(code);
} }
else if (_writeZeroCache != null) else if (_dualMappedCache != null)
{ {
return _writeZeroCache.MapPageAligned(code); return _dualMappedCache.MapPageAligned(code);
} }
else else
{ {

View File

@ -127,11 +127,12 @@ namespace Ryujinx.Cpu.Signal
ulong codeSizeAligned = BitUtils.AlignUp((ulong)code.Length, MemoryBlock.GetPageSize()); ulong codeSizeAligned = BitUtils.AlignUp((ulong)code.Length, MemoryBlock.GetPageSize());
_codeBlock = new MemoryBlock(codeSizeAligned); string dualMapped = Environment.GetEnvironmentVariable("DUAL_MAPPED_JIT");
_codeBlock = new MemoryBlock(codeSizeAligned, (dualMapped == "1") ? MemoryAllocationFlags.DualMapping : MemoryAllocationFlags.None);
_codeBlock.Write(0, code); _codeBlock.Write(0, code);
_codeBlock.Reprotect(0, codeSizeAligned, MemoryPermission.ReadAndExecute); _codeBlock.Reprotect(0, codeSizeAligned, MemoryPermission.ReadAndExecute);
return _codeBlock.Pointer; return _codeBlock.RxPointer;
} }
private static unsafe ref SignalHandlerConfig GetConfigRef() private static unsafe ref SignalHandlerConfig GetConfigRef()

View File

@ -10,7 +10,7 @@ namespace Ryujinx.Graphics.Vulkan
public const int MaxShaderStages = 5; public const int MaxShaderStages = 5;
public const int MaxUniformBuffersPerStage = 18; public const int MaxUniformBuffersPerStage = 18;
public const int MaxStorageBuffersPerStage = 16; public const int MaxStorageBuffersPerStage = 16;
public const int MaxTexturesPerStage = 18; public const int MaxTexturesPerStage = 32; // 31
public const int MaxImagesPerStage = 16; public const int MaxImagesPerStage = 16;
public const int MaxUniformBufferBindings = MaxUniformBuffersPerStage * MaxShaderStages; public const int MaxUniformBufferBindings = MaxUniformBuffersPerStage * MaxShaderStages;
public const int MaxStorageBufferBindings = MaxStorageBuffersPerStage * MaxShaderStages; public const int MaxStorageBufferBindings = MaxStorageBuffersPerStage * MaxShaderStages;

View File

@ -680,6 +680,8 @@ namespace Ryujinx.Graphics.Vulkan
ShaderCollection program = _program; ShaderCollection program = _program;
// UpdateAndBindTexturesWithoutTemplate jas been renamed to UpdateAndBind and supports more then just textures.
if (_dirty.HasFlag(DirtyFlags.Uniform)) if (_dirty.HasFlag(DirtyFlags.Uniform))
{ {
if (program.UsePushDescriptors) if (program.UsePushDescriptors)
@ -688,51 +690,77 @@ namespace Ryujinx.Graphics.Vulkan
} }
else else
{ {
UpdateAndBind(cbs, program, PipelineBase.UniformSetIndex, pbp); try {
UpdateAndBind(cbs, program, PipelineBase.UniformSetIndex, pbp);
}
catch (Exception e)
{
// If binding fails, we can try to bind the uniform buffers without using the template.
// This is a workaround for some games that use invalid bindings.
UpdateAndBind(cbs, PipelineBase.UniformSetIndex, pbp);
}
} }
} }
if (_dirty.HasFlag(DirtyFlags.Storage)) if (_dirty.HasFlag(DirtyFlags.Storage))
{ {
UpdateAndBind(cbs, program, PipelineBase.StorageSetIndex, pbp); try {
UpdateAndBind(cbs, program, PipelineBase.StorageSetIndex, pbp);
}
catch (Exception e)
{
// If binding fails, we can try to bind the storage buffers without using the template.
// This is a workaround for some games that use invalid bindings.
UpdateAndBind(cbs, PipelineBase.StorageSetIndex, pbp);
}
} }
if (_dirty.HasFlag(DirtyFlags.Texture)) if (_dirty.HasFlag(DirtyFlags.Texture))
{ {
if (true) if (program.UpdateTexturesWithoutTemplate)
{ {
try UpdateAndBind(cbs, PipelineBase.TextureSetIndex, pbp);
{
UpdateAndBind(cbs, program, PipelineBase.TextureSetIndex, pbp);
}
catch (Exception e)
{
UpdateAndBindTexturesWithoutTemplate(cbs, program, pbp);
}
} }
else else
{ {
try try
{ {
UpdateAndBind(cbs, program, PipelineBase.TextureSetIndex, pbp); UpdateAndBind(cbs, program, PipelineBase.TextureSetIndex, pbp);
} }
catch (Exception e) catch (Exception e)
{ {
UpdateAndBindTexturesWithoutTemplate(cbs, program, pbp); // If binding fails, we can try to bind the textures without using the template.
// This is a workaround for some games that use invalid bindings.
UpdateAndBind(cbs, PipelineBase.TextureSetIndex, pbp);
} }
} }
} }
if (_dirty.HasFlag(DirtyFlags.Image)) if (_dirty.HasFlag(DirtyFlags.Image))
{ {
UpdateAndBind(cbs, program, PipelineBase.ImageSetIndex, pbp); try
{
UpdateAndBind(cbs, program, PipelineBase.ImageSetIndex, pbp);
}
catch (Exception e)
{
// If binding fails, we can try to bind the images without using the template.
// This is a workaround for some games that use invalid bindings.
UpdateAndBind(cbs, PipelineBase.ImageSetIndex, pbp);
}
} }
if (program.BindingSegments.Length > PipelineBase.DescriptorSetLayouts) if (program.BindingSegments.Length > PipelineBase.DescriptorSetLayouts)
{ {
// Program is using extra sets, we need to bind those too. // Program is using extra sets, we need to bind those too.
BindExtraSets(cbs, program, pbp); // Ignore Extra Sets for now.
// BindExtraSets(cbs, program, pbp);
} }
_dirty = DirtyFlags.None; _dirty = DirtyFlags.None;
@ -764,7 +792,7 @@ namespace Ryujinx.Graphics.Vulkan
if (info.Buffer.Handle == 0) if (info.Buffer.Handle == 0)
{ {
info.Buffer = dummyBuffer?.Get(cbs).Value ?? default; info.Buffer = dummyBuffer?.Get(cbs).Value ?? default;
// info.Offset = 0; info.Offset = 0;
info.Range = Vk.WholeSize; info.Range = Vk.WholeSize;
} }
@ -962,9 +990,9 @@ namespace Ryujinx.Graphics.Vulkan
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateAndBindTexturesWithoutTemplate(CommandBufferScoped cbs, ShaderCollection program, PipelineBindPoint pbp) private void UpdateAndBind(CommandBufferScoped cbs, int setIndex, PipelineBindPoint pbp)
{ {
int setIndex = PipelineBase.TextureSetIndex; var program = _program;
var bindingSegments = program.BindingSegments[setIndex]; var bindingSegments = program.BindingSegments[setIndex];
if (bindingSegments.Length == 0) if (bindingSegments.Length == 0)
@ -972,6 +1000,24 @@ namespace Ryujinx.Graphics.Vulkan
return; return;
} }
var dummyBuffer = _dummyBuffer?.GetBuffer();
if (_updateDescriptorCacheCbIndex)
{
_updateDescriptorCacheCbIndex = false;
program.UpdateDescriptorCacheCommandBufferIndex(cbs.CommandBufferIndex);
}
var dsc = program.GetNewDescriptorSetCollection(setIndex, out var isNew).Get(cbs);
if (!program.HasMinimalLayout)
{
if (isNew)
{
Initialize(cbs, setIndex, dsc);
}
}
var dummyImageInfo = new DescriptorImageInfo var dummyImageInfo = new DescriptorImageInfo
{ {
ImageView = _dummyTexture.GetImageView().Get(cbs).Value, ImageView = _dummyTexture.GetImageView().Get(cbs).Value,
@ -979,89 +1025,160 @@ namespace Ryujinx.Graphics.Vulkan
ImageLayout = ImageLayout.General ImageLayout = ImageLayout.General
}; };
var dsc = program.GetNewDescriptorSetCollection(setIndex, out _).Get(cbs);
foreach (ResourceBindingSegment segment in bindingSegments) foreach (ResourceBindingSegment segment in bindingSegments)
{ {
int binding = segment.Binding; int binding = segment.Binding;
int count = segment.Count; int count = segment.Count;
if (!segment.IsArray) if (setIndex == PipelineBase.UniformSetIndex)
{ {
if (segment.Type != ResourceType.BufferTexture) for (int i = 0; i < count; i++)
{ {
for (int i = 0; i < count; i++) int index = binding + i;
if (_uniformSet.Set(index))
{ {
int index = binding + i; ref BufferRef buffer = ref _uniformBufferRefs[index];
ref var textureRef = ref _textureRefs[index];
var imageView = textureRef.ImageView?.Get(cbs).Value ?? dummyImageInfo.ImageView; bool mirrored = UpdateBuffer(cbs, ref _uniformBuffers[index], ref buffer, dummyBuffer, true);
var sampler = textureRef.Sampler?.Get(cbs).Value ?? dummyImageInfo.Sampler;
var imageInfo = new DescriptorImageInfo _uniformMirrored.Set(index, mirrored);
}
}
ReadOnlySpan<DescriptorBufferInfo> uniformBuffers = _uniformBuffers;
dsc.UpdateBuffers(0, binding, uniformBuffers.Slice(binding, count), DescriptorType.UniformBuffer);
}
else if (setIndex == PipelineBase.StorageSetIndex)
{
for (int i = 0; i < count; i++)
{
int index = binding + i;
ref BufferRef buffer = ref _storageBufferRefs[index];
if (_storageSet.Set(index))
{
ref var info = ref _storageBuffers[index];
bool mirrored = UpdateBuffer(cbs,
ref info,
ref _storageBufferRefs[index],
dummyBuffer,
!buffer.Write && info.Range <= StorageBufferMaxMirrorable);
_storageMirrored.Set(index, mirrored);
}
}
ReadOnlySpan<DescriptorBufferInfo> storageBuffers = _storageBuffers;
dsc.UpdateBuffers(0, binding, storageBuffers.Slice(binding, count), DescriptorType.StorageBuffer);
}
else if (setIndex == PipelineBase.TextureSetIndex)
{
if (!segment.IsArray)
{
if (segment.Type != ResourceType.BufferTexture)
{
for (int i = 0; i < count; i++)
{ {
ImageView = imageView.Handle != 0 ? imageView : dummyImageInfo.ImageView, int index = binding + i;
Sampler = sampler.Handle != 0 ? sampler : dummyImageInfo.Sampler, ref var textureRef = ref _textureRefs[index];
ImageLayout = ImageLayout.General
};
dsc.UpdateImages(0, index, new[] { imageInfo }, DescriptorType.CombinedImageSampler); var imageView = textureRef.ImageView?.Get(cbs).Value ?? dummyImageInfo.ImageView;
var sampler = textureRef.Sampler?.Get(cbs).Value ?? dummyImageInfo.Sampler;
var imageInfo = new DescriptorImageInfo
{
ImageView = imageView.Handle != 0 ? imageView : dummyImageInfo.ImageView,
Sampler = sampler.Handle != 0 ? sampler : dummyImageInfo.Sampler,
ImageLayout = ImageLayout.General
};
dsc.UpdateImages(0, index, new[] { imageInfo }, DescriptorType.CombinedImageSampler);
}
}
else
{
for (int i = 0; i < count; i++)
{
int index = binding + i;
var bufferView = _bufferTextureRefs[index]?.GetBufferView(cbs, false) ?? default(BufferView);
dsc.UpdateBufferImages(0, index, new[] { bufferView }, DescriptorType.UniformTexelBuffer);
}
} }
} }
else else
{ {
for (int i = 0; i < count; i++) var arrayRef = _textureArrayRefs[binding];
if (segment.Type != ResourceType.BufferTexture)
{ {
int index = binding + i; var imageInfos = arrayRef.Array.GetImageInfos(_gd, cbs, _dummyTexture, _dummySampler);
var bufferView = _bufferTextureRefs[index]?.GetBufferView(cbs, false) ?? default; if (imageInfos != null)
dsc.UpdateBufferImages(0, index, new[] { bufferView }, DescriptorType.UniformTexelBuffer); {
for (int i = 0; i < imageInfos.Length && i < count; i++)
{
dsc.UpdateImages(0, binding + i, new[] { imageInfos[i] }, DescriptorType.CombinedImageSampler);
}
}
else
{
for (int i = 0; i < count; i++)
{
dsc.UpdateImages(0, binding + i, new[] { dummyImageInfo }, DescriptorType.CombinedImageSampler);
}
}
}
else
{
var bufferViews = arrayRef.Array.GetBufferViews(cbs);
if (bufferViews != null)
{
for (int i = 0; i < bufferViews.Length && i < count; i++)
{
dsc.UpdateBufferImages(0, binding + i, new[] { bufferViews[i] }, DescriptorType.UniformTexelBuffer);
}
}
else
{
for (int i = 0; i < count; i++)
{
dsc.UpdateBufferImages(0, binding + i, new[] { default(BufferView) }, DescriptorType.UniformTexelBuffer);
}
}
} }
} }
} }
else else if (setIndex == PipelineBase.ImageSetIndex)
{ {
var arrayRef = _textureArrayRefs[binding]; if (segment.Type != ResourceType.BufferImage)
if (segment.Type != ResourceType.BufferTexture)
{ {
var imageInfos = arrayRef.Array.GetImageInfos(_gd, cbs, _dummyTexture, _dummySampler); Span<DescriptorImageInfo> images = _images;
if (imageInfos != null)
for (int i = 0; i < count; i++)
{ {
for (int i = 0; i < imageInfos.Length && i < count; i++) images[i].ImageView = _imageRefs[binding + i].ImageView?.Get(cbs).Value ?? dummyImageInfo.ImageView;
{
dsc.UpdateImages(0, binding + i, new[] { imageInfos[i] }, DescriptorType.CombinedImageSampler);
}
}
else
{
for (int i = 0; i < count; i++)
{
dsc.UpdateImages(0, binding + i, new[] { dummyImageInfo }, DescriptorType.CombinedImageSampler);
}
} }
dsc.UpdateImages(0, binding, images[..count], DescriptorType.StorageImage);
} }
else else
{ {
var bufferViews = arrayRef.Array.GetBufferViews(cbs); Span<BufferView> bufferImages = _bufferImages;
if (bufferViews != null)
for (int i = 0; i < count; i++)
{ {
for (int i = 0; i < bufferViews.Length && i < count; i++) bufferImages[i] = _bufferImageRefs[binding + i]?.GetBufferView(cbs, true) ?? default;
{
dsc.UpdateBufferImages(0, binding + i, new[] { bufferViews[i] }, DescriptorType.UniformTexelBuffer);
}
}
else
{
for (int i = 0; i < count; i++)
{
dsc.UpdateBufferImages(0, binding + i, new[] { default(BufferView) }, DescriptorType.UniformTexelBuffer);
}
} }
dsc.UpdateBufferImages(0, binding, bufferImages[..count], DescriptorType.StorageTexelBuffer);
} }
} }
} }
var sets = dsc.GetSets(); var sets = dsc.GetSets();
_gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, _program.PipelineLayout, (uint)setIndex, 1, sets, 0, ReadOnlySpan<uint>.Empty); _gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, _program.PipelineLayout, (uint)setIndex, 1, sets, 0, ReadOnlySpan<uint>.Empty);
} }
@ -1232,4 +1349,4 @@ namespace Ryujinx.Graphics.Vulkan
Dispose(true); Dispose(true);
} }
} }
} }

View File

@ -186,8 +186,6 @@ namespace Ryujinx.Graphics.Vulkan
return sets; return sets;
} }
var dsc = program.GetNewDescriptorSetCollection(setIndex, out var isNew).Get(cbs);
DescriptorSetTemplate template = program.Templates[setIndex]; DescriptorSetTemplate template = program.Templates[setIndex];
DescriptorSetTemplateWriter tu = templateUpdater.Begin(template); DescriptorSetTemplateWriter tu = templateUpdater.Begin(template);
@ -203,8 +201,6 @@ namespace Ryujinx.Graphics.Vulkan
templateUpdater.Commit(_gd, device, sets[0]); templateUpdater.Commit(_gd, device, sets[0]);
sets = dsc.GetSets();
return sets; return sets;
} }
} }

View File

@ -11,7 +11,7 @@ namespace Ryujinx.Graphics.Vulkan
{ {
public static unsafe DisposableRenderPass ToRenderPass(this ProgramPipelineState state, VulkanRenderer gd, Device device) public static unsafe DisposableRenderPass ToRenderPass(this ProgramPipelineState state, VulkanRenderer gd, Device device)
{ {
const int MaxAttachments = Constants.MaxRenderTargets + 1; int MaxAttachments = Constants.MaxRenderTargets + 1;
AttachmentDescription[] attachmentDescs = null; AttachmentDescription[] attachmentDescs = null;
@ -185,8 +185,8 @@ namespace Ryujinx.Graphics.Vulkan
if (gd.Capabilities.SupportsMultiView) if (gd.Capabilities.SupportsMultiView)
{ {
pipeline.ScissorsCount = Constants.MaxViewports; pipeline.ScissorsCount = (uint)Constants.MaxViewports;
pipeline.ViewportsCount = Constants.MaxViewports; pipeline.ViewportsCount = (uint)Constants.MaxViewports;
} }
else else
{ {

View File

@ -137,7 +137,7 @@ namespace Ryujinx.Graphics.Vulkan
(IncoherentBufferWriteStages, IncoherentTextureWriteStages) = BuildIncoherentStages(resourceLayout.SetUsages); (IncoherentBufferWriteStages, IncoherentTextureWriteStages) = BuildIncoherentStages(resourceLayout.SetUsages);
// Updating buffer texture bindings using template updates crashes the Adreno driver on Windows. // Updating buffer texture bindings using template updates crashes the Adreno driver on Windows.
UpdateTexturesWithoutTemplate = OperatingSystem.IsIOS(); // gd.IsQualcommProprietary && usesBufferTextures; UpdateTexturesWithoutTemplate = gd.IsQualcommProprietary && usesBufferTextures;
_compileTask = Task.CompletedTask; _compileTask = Task.CompletedTask;
_firstBackgroundUse = false; _firstBackgroundUse = false;

View File

@ -402,9 +402,7 @@ namespace Ryujinx.Graphics.Vulkan
properties.Limits.FramebufferDepthSampleCounts & properties.Limits.FramebufferDepthSampleCounts &
properties.Limits.FramebufferStencilSampleCounts; properties.Limits.FramebufferStencilSampleCounts;
bool isDynamicStateSupported = OperatingSystem.IsIOS() bool isDynamicStateSupported = OperatingSystem.IsIOSVersionAtLeast(17) && _physicalDevice.IsDeviceExtensionPresent(ExtExtendedDynamicState.ExtensionName);
? OperatingSystem.IsIOSVersionAtLeast(17) && _physicalDevice.IsDeviceExtensionPresent(ExtExtendedDynamicState.ExtensionName)
: _physicalDevice.IsDeviceExtensionPresent(ExtExtendedDynamicState.ExtensionName);
Capabilities = new HardwareCapabilities( Capabilities = new HardwareCapabilities(
_physicalDevice.IsDeviceExtensionPresent("VK_EXT_index_type_uint8"), _physicalDevice.IsDeviceExtensionPresent("VK_EXT_index_type_uint8"),
@ -422,8 +420,8 @@ namespace Ryujinx.Graphics.Vulkan
features2.Features.ShaderStorageImageMultisample, features2.Features.ShaderStorageImageMultisample,
_physicalDevice.IsDeviceExtensionPresent(ExtConditionalRendering.ExtensionName), _physicalDevice.IsDeviceExtensionPresent(ExtConditionalRendering.ExtensionName),
isDynamicStateSupported, isDynamicStateSupported,
features2.Features.MultiViewport && !IsMoltenVk, // Workaround for AMD on MoltenVK issue features2.Features.MultiViewport, // && !IsMoltenVk, // Workaround for AMD on MoltenVK issue
!IsMoltenVk ? featuresRobustness2.NullDescriptor : false, featuresRobustness2.NullDescriptor && !IsMoltenVk,
supportsPushDescriptors && !IsMoltenVk, supportsPushDescriptors && !IsMoltenVk,
propertiesPushDescriptor.MaxPushDescriptors, propertiesPushDescriptor.MaxPushDescriptors,
featuresPrimitiveTopologyListRestart.PrimitiveTopologyListRestart, featuresPrimitiveTopologyListRestart.PrimitiveTopologyListRestart,
@ -649,8 +647,7 @@ namespace Ryujinx.Graphics.Vulkan
if (!OperatingSystem.IsIOSVersionAtLeast(16, 4)) if (!OperatingSystem.IsIOSVersionAtLeast(16, 4))
{ {
// On iOS 16.4 and later, these formats are supported. // On iOS 16.3.1 and earlier, these formats are not supported.
// On earlier versions, it is not supported.
supportsBc123CompressionFormat = false; supportsBc123CompressionFormat = false;
supportsBc45CompressionFormat = false; supportsBc45CompressionFormat = false;
supportsBc67CompressionFormat = false; supportsBc67CompressionFormat = false;
@ -723,7 +720,7 @@ namespace Ryujinx.Graphics.Vulkan
SystemMemoryType memoryType; SystemMemoryType memoryType;
if (IsSharedMemory && !IsMoltenVk) if (IsSharedMemory)
{ {
memoryType = SystemMemoryType.UnifiedMemory; memoryType = SystemMemoryType.UnifiedMemory;
} }
@ -785,10 +782,10 @@ namespace Ryujinx.Graphics.Vulkan
imageSetIndex: PipelineBase.ImageSetIndex, imageSetIndex: PipelineBase.ImageSetIndex,
extraSetBaseIndex: PipelineBase.DescriptorSetLayouts, extraSetBaseIndex: PipelineBase.DescriptorSetLayouts,
maximumExtraSets: Math.Max(0, (int)limits.MaxBoundDescriptorSets - PipelineBase.DescriptorSetLayouts), maximumExtraSets: Math.Max(0, (int)limits.MaxBoundDescriptorSets - PipelineBase.DescriptorSetLayouts),
maximumUniformBuffersPerStage: Constants.MaxUniformBuffersPerStage, maximumUniformBuffersPerStage: (uint)Constants.MaxUniformBuffersPerStage,
maximumStorageBuffersPerStage: Constants.MaxStorageBuffersPerStage, maximumStorageBuffersPerStage: (uint)Constants.MaxStorageBuffersPerStage,
maximumTexturesPerStage: Constants.MaxTexturesPerStage, maximumTexturesPerStage: (uint)Constants.MaxTexturesPerStage,
maximumImagesPerStage: Constants.MaxImagesPerStage, maximumImagesPerStage: (uint)Constants.MaxImagesPerStage,
maximumComputeSharedMemorySize: (int)limits.MaxComputeSharedMemorySize, maximumComputeSharedMemorySize: (int)limits.MaxComputeSharedMemorySize,
maximumSupportedAnisotropy: (int)limits.MaxSamplerAnisotropy, maximumSupportedAnisotropy: (int)limits.MaxSamplerAnisotropy,
shaderSubgroupSize: (int)Capabilities.SubgroupSize, shaderSubgroupSize: (int)Capabilities.SubgroupSize,

View File

@ -22,7 +22,7 @@ namespace Ryujinx.HLE.HOS.Services.Nifm.StaticService
_generalServiceDetail = new GeneralServiceDetail _generalServiceDetail = new GeneralServiceDetail
{ {
ClientId = GeneralServiceManager.Count, ClientId = GeneralServiceManager.Count,
IsAnyInternetRequestAccepted = true, // NOTE: Why not accept any internet request? IsAnyInternetRequestAccepted = false, // NOTE: Why not accept any internet request?
}; };
NetworkChange.NetworkAddressChanged += LocalInterfaceCacheHandler; NetworkChange.NetworkAddressChanged += LocalInterfaceCacheHandler;

View File

@ -992,15 +992,16 @@ namespace Ryujinx.Headless.SDL2
bool isNintendoStyle = true; // gamepadName.Contains("Nintendo") || gamepadName.Contains("Joycons"); bool isNintendoStyle = true; // gamepadName.Contains("Nintendo") || gamepadName.Contains("Joycons");
ControllerType currentController; ControllerType currentController;
if (index == PlayerIndex.Handheld) if (index == PlayerIndex.Handheld)
{ {
currentController = ControllerType.Handheld; currentController = ControllerType.Handheld;
} }
else if (gamepadName.Contains("Joycons") || gamepadName.Contains("Backbone")) else if (gamepadName.Contains("Joycons") || gamepadName.Contains("Backbone"))
{ {
currentController = ControllerType.JoyconPair; currentController = ControllerType.JoyconPair;
} }
else else
{ {
currentController = ControllerType.ProController; currentController = ControllerType.ProController;
} }

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
@ -48,7 +48,7 @@
<Message Importance="normal" Text="Found XCode at $(XcodeSelect)" Condition=" '$(FindXCode)' == 'true' "/> <Message Importance="normal" Text="Found XCode at $(XcodeSelect)" Condition=" '$(FindXCode)' == 'true' "/>
<ItemGroup> <ItemGroup>
<!-- <LinkerArg Include="-Wl,-ld_classic" /> --> <!-- <LinkerArg Include="-Wl,-ld_classic" /> -->
<LinkerArg Include="-flto -Ofast" /> <LinkerArg Include="-flto -Ofast" />
<LinkerArg Include="-isysroot %22$(XCodePath)Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk%22" <LinkerArg Include="-isysroot %22$(XCodePath)Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk%22"
Condition=" $(RuntimeIdentifier.Contains('simulator')) "/> Condition=" $(RuntimeIdentifier.Contains('simulator')) "/>

View File

@ -0,0 +1,53 @@
using System;
using System.Runtime.InteropServices;
using System.Diagnostics;
using Ryujinx.Common.Logging;
namespace Ryujinx.Memory
{
/// <summary>
/// Placeholder class for JIT memory allocation on iOS.
/// Intended to allocate memory with both r/x and r/w permissions,
/// as a workaround for stricter W^X (Write XOR Execute) enforcement introduced in iOS 26.
///
/// Specifically targets iOS 26, where the traditional method of reprotecting
/// memory from writable to executable (RX) no longer works for JIT code.
///
/// The actual allocation logic will be implemented after the release of iOS 26
/// to reduce the risk of this workaround being patched.
/// </summary>
public class DualMappedJitAllocator : IDisposable
{
public IntPtr RwPtr { get; private set; }
public IntPtr RxPtr { get; private set; }
public ulong Size { get; private set; }
private IntPtr _mmapPtr;
public DualMappedJitAllocator(ulong size)
{
var stackTrace = new StackTrace(1, false);
var callingMethod = stackTrace.GetFrame(0)?.GetMethod();
Logger.Info?.Print(LogClass.Cpu,
$"Allocating dual-mapped JIT memory of size {size} bytes, called by {callingMethod?.DeclaringType?.FullName}.{callingMethod?.Name}");
Size = size;
AllocateDualMapping();
}
private void AllocateDualMapping()
{
RwPtr = IntPtr.Zero;
RxPtr = IntPtr.Zero;
}
public void Dispose()
{
}
}
}

View File

@ -48,5 +48,11 @@ namespace Ryujinx.Memory
/// On some platforms, this requires special flags to be passed that will allow the memory to be executable. /// On some platforms, this requires special flags to be passed that will allow the memory to be executable.
/// </summary> /// </summary>
Jit = 1 << 5, Jit = 1 << 5,
/// <summary>
/// Indicates that the memory will be used to store JIT generated code in both read and execute modes.
/// On some platforms, this is required to allow the JIT to generate code that can be executed.
/// </summary>
DualMapping = 1 << 6,
} }
} }

View File

@ -13,14 +13,21 @@ namespace Ryujinx.Memory
private readonly bool _isMirror; private readonly bool _isMirror;
private readonly bool _viewCompatible; private readonly bool _viewCompatible;
private readonly bool _forJit; private readonly bool _forJit;
private DualMappedJitAllocator _dualMappedAllocator;
private IntPtr _sharedMemory; private IntPtr _sharedMemory;
private IntPtr _pointer; private IntPtr _pointer;
private IntPtr _rxPointer;
/// <summary> /// <summary>
/// Pointer to the memory block data. /// Pointer to the memory block data (RW).
/// </summary> /// </summary>
public IntPtr Pointer => _pointer; public IntPtr Pointer => _pointer;
/// <summary>
/// Pointer to the RX mapping (for execution), or IntPtr.Zero if not dual-mapped.
/// </summary>
public IntPtr RxPointer => _rxPointer;
/// <summary> /// <summary>
/// Size of the memory block. /// Size of the memory block.
/// </summary> /// </summary>
@ -35,7 +42,16 @@ namespace Ryujinx.Memory
/// <exception cref="PlatformNotSupportedException">Throw when the current platform is not supported</exception> /// <exception cref="PlatformNotSupportedException">Throw when the current platform is not supported</exception>
public MemoryBlock(ulong size, MemoryAllocationFlags flags = MemoryAllocationFlags.None) public MemoryBlock(ulong size, MemoryAllocationFlags flags = MemoryAllocationFlags.None)
{ {
if (flags.HasFlag(MemoryAllocationFlags.Mirrorable)) Size = size;
if (flags.HasFlag(MemoryAllocationFlags.DualMapping))
{
_dualMappedAllocator = new DualMappedJitAllocator(size);
_pointer = _dualMappedAllocator.RwPtr;
_rxPointer = _dualMappedAllocator.RxPtr;
_forJit = true;
return;
}
else if (flags.HasFlag(MemoryAllocationFlags.Mirrorable))
{ {
_sharedMemory = MemoryManagement.CreateSharedMemory(size, flags.HasFlag(MemoryAllocationFlags.Reserve)); _sharedMemory = MemoryManagement.CreateSharedMemory(size, flags.HasFlag(MemoryAllocationFlags.Reserve));
@ -58,7 +74,7 @@ namespace Ryujinx.Memory
_pointer = MemoryManagement.Allocate(size, _forJit); _pointer = MemoryManagement.Allocate(size, _forJit);
} }
Size = size; _rxPointer = _pointer;
} }
/// <summary> /// <summary>
@ -165,7 +181,10 @@ namespace Ryujinx.Memory
/// <exception cref="MemoryProtectionException">Throw when <paramref name="permission"/> is invalid</exception> /// <exception cref="MemoryProtectionException">Throw when <paramref name="permission"/> is invalid</exception>
public void Reprotect(ulong offset, ulong size, MemoryPermission permission, bool throwOnFail = true) public void Reprotect(ulong offset, ulong size, MemoryPermission permission, bool throwOnFail = true)
{ {
MemoryManagement.Reprotect(GetPointerInternal(offset, size), size, permission, _viewCompatible, throwOnFail); if (_rxPointer == _pointer)
{
MemoryManagement.Reprotect(GetPointerInternal(offset, size), size, permission, _viewCompatible, throwOnFail);
}
} }
/// <summary> /// <summary>
@ -388,8 +407,13 @@ namespace Ryujinx.Memory
{ {
IntPtr ptr = Interlocked.Exchange(ref _pointer, IntPtr.Zero); IntPtr ptr = Interlocked.Exchange(ref _pointer, IntPtr.Zero);
// If pointer is null, the memory was already freed or never allocated. if (_dualMappedAllocator != null)
if (ptr != IntPtr.Zero) {
_dualMappedAllocator.Dispose();
_dualMappedAllocator = null;
_rxPointer = IntPtr.Zero;
}
else if (ptr != IntPtr.Zero)
{ {
if (_usesSharedMemory) if (_usesSharedMemory)
{ {