` with your actual name or identifier.)*
-### 5. Connect Your Device
+### 4. Connect Your Device
Ensure your **iPhone/iPad** is **connected** and **selected** (Next to MeloNX with the arrow) in Xcode.
- You may need to install the iOS SDK. it will say next to MeloNX with the arrow saying "iOS XX Not Installed (GET)"
- You will be need to press GET and wait for it to finish downloading and installing
- Then you will be able to select your device and Build and Run.
-Make Sure you do **NOT** select the Simulator. (Which is the Generic names and the ones with the non-coloured icons, e.g. "iPhone 16 Pro")
+### Make Sure you do **NOT** select the Simulator. (Which is the Generic names and the ones with the non-coloured icons, e.g. "iPhone 16 Pro")
-### 6. Build and Run
+### 6. Configure the Project Settings
+
+Click the **Run (▶️) button** in Xcode to compile MeloNX and wait it will fail with Undefined Symbol(s) the first time, Thats normal.
+- When it fails the first time, do this:
+- In **Xcode**, select the **MeloNX** project.
+- Under the **General** tab, find `Ryujinx.Headless.SDL2.dylib`.
+- Set its **Embed setting** to **"Embed & Sign"**.
+
+### 5. Build and Run
Click the **Run (▶️) button** in Xcode to compile and launch MeloNX.
- When running on your device, Click the **Spray Can Button** below the Run button
- Right Click where it says "> MeloNX PID XXXX"
- Press Detach in the Context Menu.
+
---
-Now you're all set! 🚀 If you encounter issues, please join the discord at https://melonx.org
-```
\ No newline at end of file
+Now you're all set! 🚀 If you encounter issues, please join the discord at https://melonx.org
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 40275763b..301024cf8 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,52 +3,50 @@
true
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
-
-
+
+
-
-
-
-
+
+
+
+
-
+
-
-
-
-
+
+
+
+
-
-
+
+
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
index db65d8553..05c6078dd 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,3 +1,12 @@
+Currently licensed under the GNU AFFERO GENERAL PUBLIC LICENSE version 3, or any later version, at your choice.
+You may obtain a copy of the license at
+
+Copyright (c) Rhajune Park and contributors, 2025
+
+For copyright infringement claims, please contact abuse@pythonplayer123.dev for expedited processing
+
+Previously licensed under the MeloNX License.
+
MeloNX License
Copyright (c) MeloNX Team and Contributors
diff --git a/README.md b/README.md
index 290cd2990..640527850 100644
--- a/README.md
+++ b/README.md
@@ -6,8 +6,6 @@
MeloNX
-
-
MeloNX enables Nintendo Switch game emulation on iOS using the Ryujinx iOS code base.
@@ -24,13 +22,16 @@ MeloNX works on iPhone XS/XR and later and iPad 8th Gen and later. Check out the
# Usage
## FAQ
-- MeloNX is made for iOS 17+, on iOS 15 - 16 MeloNX can be installed but will have issues or may not work at all.
-- MeloNX needs Xcode or a Paid Apple Developer Account. SideStore support may come soon (SideStore Side Issue)
-- MeloNX needs JIT
+- 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 requires JIT
- Recommended Device: iPhone 15 Pro or newer.
-- Low-End Recommended Device**: iPhone 13 Pro.
-- Lowest Supported Device: iPhone XR
+- Low-End Recommended Device: iPhone 13 Pro.
+## Discord Server
+
+We have a discord server!
+ - https://discord.gg/melonx
## How to install
@@ -51,14 +52,67 @@ MeloNX works on iPhone XS/XR and later and iPad 8th Gen and later. Check out the
4. **Enable JIT**
- Use your preferred method to enable Just-In-Time (JIT) compilation.
+ - 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**.
+ - If needed, install firmware and keys from **Ryujinx Desktop** (or forks).
- Copy the **bis** and **system** folders
+
+### Free Developer Account (Experimental)
-### Xcode
+1. **Sideload MeloNX**
+ - Use [SideStore](https://sidestore.io/) or [AltStore](https://altstore.io/) (**NOT** AltStore PAL).
+
+2. **Sideload the Entitlement App**
+ - Install [this app](https://github.com/hugeBlack/GetMoreRam/releases/download/nightly/Entitlement.ipa) using [SideStore](https://sidestore.io/) or [AltStore](https://altstore.io/) (**NOT** AltStore PAL).
+
+3. **Sign In to Your Account**
+ - Open **Settings** in the entitlement app and sign in with your Apple ID.
+
+4. **Refresh App IDs**
+ - Navigate to the **App IDs** page.
+ - Tap **Refresh** to update the list.
+
+5. **Enable Increased Memory Limit**
+ - Select **MeloNX** (should be like "com.stossy11.MeloNX" or some variation) from the list.
+ - Tap **Add Increased Memory Limit**.
+
+6. **Reinstall MeloNX**
+ - Delete the existing installation.
+ - Sideload the app again using SideStore or AltStore.
+
+7. **Verify Increased Memory Limit**
+ - Open MeloNX and check if the **Increased Memory Limit** is enabled.
+
+8. **Add Necessary Files**
+
+If having Issues installing firmware (Make sure your keys are installed first)
+ - If needed, install firmware and keys from **Ryujinx Desktop** (or forks).
+ - Copy the **bis** and **system** folders
+
+9. **Enable JIT**
+ - Use your preferred method to enable Just-In-Time (JIT) compilation.
+ - We recommend using [StikDebug](https://apps.apple.com/us/app/stikdebug/id6744045754)
+
+
+### TrollStore
+As Said in FAQ:
+> MeloNX is made for iOS 17+, on iOS 15 - 16 MeloNX can be installed but may have issues or not work at all.
+
+1. **Install MeloNX with TrollStore**
+
+2. **Add Necessary Files**
+
+3. **Enable TrollStore JIT**
+ - MeloNX includes automatic JIT using the TrollStore URL Scheme
+ - Open MeloNX Settings
+ - Scroll down and enable the "TrollStore JIT" toggle
+ - Profit
+
+
+### Free Developer Account (Xcode)
**NOTE: These Xcode builds are nightly and may have unfinished features.**
@@ -67,8 +121,8 @@ If having Issues installing firmware (Make sure your Keys are installed first)
2. **Add Necessary Files**
-If having Issues installing firmware (Make sure your Keys are installed first)
- - If needed, install firmware and keys from **Ryujinx Desktop**.
+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
## Features
@@ -91,31 +145,29 @@ If having Issues installing firmware (Make sure your Keys are installed first)
- **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**
- We currently have support for keyboard, touch input, JoyCon input support, and nearly all controllers.
- Motion controls are natively supported in most cases; for dual-JoyCon motion support, DS4Windows or BetterJoy are currently required.
- In all scenarios, you can set up everything inside the input configuration menu.
-
+ We currently have support for keyboard, touch input, JoyCon input support, and nearly all MFI controllers.
+ Motion controls are natively supported in most cases, however JoyCons do not have motion support doe to an iOS limitation.
+
- **DLC & Modifications**
- MeloNX does not support add-on content/downloadable content.
+ MeloNX supports DLC + Game Update Add-ons.
Mods (romfs, exefs, and runtime mods such as cheats) are supported;
- **Configuration**
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.
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)
- [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system.
- [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation.
diff --git a/Ryujinx.sln b/Ryujinx.sln
index bb196cabc..76ebd573f 100644
--- a/Ryujinx.sln
+++ b/Ryujinx.sln
@@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32228.430
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx", "src\Ryujinx\Ryujinx.csproj", "{074045D4-3ED2-4711-9169-E385F2BFB5A0}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Gtk3", "src\Ryujinx.Gtk3\Ryujinx.Gtk3.csproj", "{074045D4-3ED2-4711-9169-E385F2BFB5A0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Tests", "src\Ryujinx.Tests\Ryujinx.Tests.csproj", "{EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}"
EndProject
@@ -69,9 +69,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Headless.SDL2", "sr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Nvdec.FFmpeg", "src\Ryujinx.Graphics.Nvdec.FFmpeg\Ryujinx.Graphics.Nvdec.FFmpeg.csproj", "{BEE1C184-C9A4-410B-8DFC-FB74D5C93AEB}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Ava", "src\Ryujinx.Ava\Ryujinx.Ava.csproj", "{7C1B2721-13DA-4B62-B046-C626605ECCE6}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx", "src\Ryujinx\Ryujinx.csproj", "{7C1B2721-13DA-4B62-B046-C626605ECCE6}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Ui.Common", "src\Ryujinx.Ui.Common\Ryujinx.Ui.Common.csproj", "{BA161CA0-CD65-4E6E-B644-51C8D1E542DC}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.UI.Common", "src\Ryujinx.UI.Common\Ryujinx.UI.Common.csproj", "{BA161CA0-CD65-4E6E-B644-51C8D1E542DC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon.Generators", "src\Ryujinx.Horizon.Generators\Ryujinx.Horizon.Generators.csproj", "{6AE2A5E8-4C5A-48B9-997B-E1455C0355C6}"
EndProject
@@ -79,7 +79,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Vulkan", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spv.Generator", "src\Spv.Generator\Spv.Generator.csproj", "{2BCB3D7A-38C0-4FE7-8FDA-374C6AD56D0E}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Ui.LocaleGenerator", "src\Ryujinx.Ui.LocaleGenerator\Ryujinx.Ui.LocaleGenerator.csproj", "{77D01AD9-2C98-478E-AE1D-8F7100738FB4}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.UI.LocaleGenerator", "src\Ryujinx.UI.LocaleGenerator\Ryujinx.UI.LocaleGenerator.csproj", "{77D01AD9-2C98-478E-AE1D-8F7100738FB4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon.Common", "src\Ryujinx.Horizon.Common\Ryujinx.Horizon.Common.csproj", "{77F96ECE-4952-42DB-A528-DED25572A573}"
EndProject
@@ -87,6 +87,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon", "src\Ryuj
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon.Kernel.Generators", "src\Ryujinx.Horizon.Kernel.Generators\Ryujinx.Horizon.Kernel.Generators.csproj", "{7F55A45D-4E1D-4A36-ADD3-87F29A285AA2}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.HLE.Generators", "src\Ryujinx.HLE.Generators\Ryujinx.HLE.Generators.csproj", "{B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -249,6 +251,10 @@ Global
{7F55A45D-4E1D-4A36-ADD3-87F29A285AA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7F55A45D-4E1D-4A36-ADD3-87F29A285AA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7F55A45D-4E1D-4A36-ADD3-87F29A285AA2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Ryujinx.sln.DotSettings b/Ryujinx.sln.DotSettings
index 049bdaf69..ed7f3e911 100644
--- a/Ryujinx.sln.DotSettings
+++ b/Ryujinx.sln.DotSettings
@@ -4,6 +4,8 @@
UseExplicitType
UseExplicitType
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy></Policy>
+ True
True
True
True
diff --git a/compile.sh b/distribution/ios/compile.sh
similarity index 94%
rename from compile.sh
rename to distribution/ios/compile.sh
index 74c158b82..22aa34495 100755
--- a/compile.sh
+++ b/distribution/ios/compile.sh
@@ -1,14 +1,15 @@
#!/bin/bash
-
# Define the destination directory (hardcoded)
DESTINATION_DIR="src/MeloNX/Dependencies/Dynamic\ Libraries/Ryujinx.Headless.SDL2.dylib"
+dotnet clean
+
# Restore the project
dotnet restore
# Build the project with the specified version
-dotnet build -c Release
+# dotnet build -c Release
# Publish the project with the specified runtime and settings
dotnet publish -c Release -r ios-arm64 -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true
diff --git a/distribution/ios/get_dotnet.sh b/distribution/ios/get_dotnet.sh
new file mode 100755
index 000000000..c5306e183
--- /dev/null
+++ b/distribution/ios/get_dotnet.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+
+# XCCONFIG_FILE="${SRCROOT}/MeloNX.xcconfig"
+
+SEARCH_PATHS=(
+ "/usr/local/share/dotnet"
+ "/usr/local/bin"
+ "/usr/bin"
+ "/bin"
+ "/opt"
+ "/Library/Frameworks"
+ "$HOME/.dotnet"
+ "$HOME/Developer"
+)
+
+
+
+DOTNET_PATH=""
+
+for path in "${SEARCH_PATHS[@]}"; do
+ if [ -d "$path" ]; then
+ DOTNET_PATH=$(find "$path" -name dotnet -type f -print -quit 2>/dev/null)
+ if [ -n "$DOTNET_PATH" ]; then
+ break
+ fi
+ fi
+done
+
+if [ -z "$DOTNET_PATH" ]; then
+ exit 1
+fi
+
+echo "$DOTNET_PATH"
diff --git a/distribution/ios/set_current_version.sh b/distribution/ios/set_current_version.sh
new file mode 100644
index 000000000..c9b713e15
--- /dev/null
+++ b/distribution/ios/set_current_version.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+
+GITEA_URL="https://git.743378673.xyz/"
+REPO="MeloNX"
+XCCONFIG_FILE="${SRCROOT}/MeloNX.xcconfig"
+
+INCREMENT_PATCH=false
+
+# Check for --patch argument
+if [[ "$1" == "--patch" ]]; then
+ INCREMENT_PATCH=true
+fi
+
+# Fetch latest tag from Gitea
+LATEST_VERSION=$(curl -s "${GITEA_URL}/api/v1/repos/${REPO}/${REPO}/tags" | jq -r '.[].name' | sort -V | tail -n1)
+
+if [ -z "$LATEST_VERSION" ]; then
+ echo "Error: Could not fetch latest tag from Gitea"
+ exit 1
+fi
+
+echo "Latest version: $LATEST_VERSION"
+
+# Split version into major, minor, and patch
+IFS='.' read -r MAJOR MINOR PATCH <<< "$LATEST_VERSION"
+
+# Increment version based on argument
+if $INCREMENT_PATCH; then
+ NEW_VERSION="$MAJOR.$MINOR.$((PATCH + 1))"
+else
+ NEW_VERSION="$MAJOR.$((MINOR + 1)).0"
+fi
+
+echo "New version: $NEW_VERSION"
+
+sed -i '' "s/^VERSION = $LATEST_VERSION$/VERSION = $NEW_VERSION/g" "$XCCONFIG_FILE"
+
+echo "Updated MeloNX.xcconfig with version $NEW_VERSION"
diff --git a/distribution/ios/xc-compile.sh b/distribution/ios/xc-compile.sh
new file mode 100755
index 000000000..3075b8e94
--- /dev/null
+++ b/distribution/ios/xc-compile.sh
@@ -0,0 +1,21 @@
+dotnet_output=$(./distribution/ios/get_dotnet.sh)
+exit_code=$?
+
+if [ $exit_code -eq 0 ]; then
+ dotnet="$dotnet_output"
+else
+ echo "error: .NET not found, Please follow the compilation instructions on the gitea." >&2
+ exit 1
+fi
+
+$dotnet publish -c Release -r ios-arm64 -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true
+
+if [ $? -ne 0 ]; then
+ echo "warning: Compiling MeloNX failed! Running dotnet clean + restore then Retrying..."
+
+ $dotnet clean
+
+ $dotnet restore
+
+ $dotnet publish -c Release -r ios-arm64 -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true
+fi
diff --git a/distribution/linux/Ryujinx.desktop b/distribution/linux/Ryujinx.desktop
index a4550d104..44f05bf3f 100644
--- a/distribution/linux/Ryujinx.desktop
+++ b/distribution/linux/Ryujinx.desktop
@@ -4,7 +4,7 @@ Name=Ryujinx
Type=Application
Icon=Ryujinx
Exec=Ryujinx.sh %f
-Comment=Plays Nintendo Switch applications
+Comment=A Nintendo Switch Emulator
GenericName=Nintendo Switch Emulator
Terminal=false
Categories=Game;Emulator;
diff --git a/distribution/linux/Ryujinx.sh b/distribution/linux/Ryujinx.sh
old mode 100644
new mode 100755
index f356cad01..30eb14399
--- a/distribution/linux/Ryujinx.sh
+++ b/distribution/linux/Ryujinx.sh
@@ -1,20 +1,23 @@
#!/bin/sh
SCRIPT_DIR=$(dirname "$(realpath "$0")")
-RYUJINX_BIN="Ryujinx"
-
-if [ -f "$SCRIPT_DIR/Ryujinx.Ava" ]; then
- RYUJINX_BIN="Ryujinx.Ava"
-fi
if [ -f "$SCRIPT_DIR/Ryujinx.Headless.SDL2" ]; then
RYUJINX_BIN="Ryujinx.Headless.SDL2"
fi
+if [ -f "$SCRIPT_DIR/Ryujinx" ]; then
+ RYUJINX_BIN="Ryujinx"
+fi
+
+if [ -z "$RYUJINX_BIN" ]; then
+ exit 1
+fi
+
COMMAND="env DOTNET_EnableAlternateStackCheck=1"
if command -v gamemoderun > /dev/null 2>&1; then
COMMAND="$COMMAND gamemoderun"
fi
-$COMMAND "$SCRIPT_DIR/$RYUJINX_BIN" "$@"
\ No newline at end of file
+exec $COMMAND "$SCRIPT_DIR/$RYUJINX_BIN" "$@"
diff --git a/distribution/macos/create_app_bundle.sh b/distribution/macos/create_app_bundle.sh
index 858c06f59..0fa54eadd 100755
--- a/distribution/macos/create_app_bundle.sh
+++ b/distribution/macos/create_app_bundle.sh
@@ -14,8 +14,8 @@ mkdir "$APP_BUNDLE_DIRECTORY/Contents/Frameworks"
mkdir "$APP_BUNDLE_DIRECTORY/Contents/MacOS"
mkdir "$APP_BUNDLE_DIRECTORY/Contents/Resources"
-# Copy executables first
-cp "$PUBLISH_DIRECTORY/Ryujinx.Ava" "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
+# Copy executable and nsure executable can be executed
+cp "$PUBLISH_DIRECTORY/Ryujinx" "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
chmod u+x "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
# Then all libraries
diff --git a/distribution/macos/create_macos_build_ava.sh b/distribution/macos/create_macos_build_ava.sh
index 80594a40a..23eafc129 100755
--- a/distribution/macos/create_macos_build_ava.sh
+++ b/distribution/macos/create_macos_build_ava.sh
@@ -22,9 +22,9 @@ EXTRA_ARGS=$8
if [ "$VERSION" == "1.1.0" ];
then
- RELEASE_TAR_FILE_NAME=test-ava-ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.app.tar
+ RELEASE_TAR_FILE_NAME=ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.app.tar
else
- RELEASE_TAR_FILE_NAME=test-ava-ryujinx-$VERSION-macos_universal.app.tar
+ RELEASE_TAR_FILE_NAME=ryujinx-$VERSION-macos_universal.app.tar
fi
ARM64_APP_BUNDLE="$TEMP_DIRECTORY/output_arm64/Ryujinx.app"
@@ -38,9 +38,9 @@ mkdir -p "$TEMP_DIRECTORY"
DOTNET_COMMON_ARGS=(-p:DebugType=embedded -p:Version="$VERSION" -p:SourceRevisionId="$SOURCE_REVISION_ID" --self-contained true $EXTRA_ARGS)
dotnet restore
-dotnet build -c "$CONFIGURATION" src/Ryujinx.Ava
-dotnet publish -c "$CONFIGURATION" -r osx-arm64 -o "$TEMP_DIRECTORY/publish_arm64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx.Ava
-dotnet publish -c "$CONFIGURATION" -r osx-x64 -o "$TEMP_DIRECTORY/publish_x64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx.Ava
+dotnet build -c "$CONFIGURATION" src/Ryujinx
+dotnet publish -c "$CONFIGURATION" -r osx-arm64 -o "$TEMP_DIRECTORY/publish_arm64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx
+dotnet publish -c "$CONFIGURATION" -r osx-x64 -o "$TEMP_DIRECTORY/publish_x64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx
# Get rid of the support library for ARMeilleure for x64 (that's only for arm64)
rm -rf "$TEMP_DIRECTORY/publish_x64/libarmeilleure-jitsupport.dylib"
@@ -108,6 +108,13 @@ tar --exclude "Ryujinx.app/Contents/MacOS/Ryujinx" -cvf "$RELEASE_TAR_FILE_NAME"
python3 "$BASE_DIR/distribution/misc/add_tar_exec.py" "$RELEASE_TAR_FILE_NAME" "Ryujinx.app/Contents/MacOS/Ryujinx" "Ryujinx.app/Contents/MacOS/Ryujinx"
gzip -9 < "$RELEASE_TAR_FILE_NAME" > "$RELEASE_TAR_FILE_NAME.gz"
rm "$RELEASE_TAR_FILE_NAME"
+
+# Create legacy update package for Avalonia to not left behind old testers.
+if [ "$VERSION" != "1.1.0" ];
+then
+ cp $RELEASE_TAR_FILE_NAME.gz test-ava-ryujinx-$VERSION-macos_universal.app.tar.gz
+fi
+
popd
echo "Done"
\ No newline at end of file
diff --git a/distribution/macos/shortcut-launch-script.sh b/distribution/macos/shortcut-launch-script.sh
new file mode 100644
index 000000000..784d780aa
--- /dev/null
+++ b/distribution/macos/shortcut-launch-script.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+launch_arch="$(uname -m)"
+if [ "$(sysctl -in sysctl.proc_translated)" = "1" ]
+then
+ launch_arch="arm64"
+fi
+
+arch -$launch_arch {0} {1}
diff --git a/docs/README.md b/docs/README.md
index 2213086f6..a22da3c7c 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -33,8 +33,3 @@ Project Docs
=================
To be added. Many project files will contain basic XML docs for key functions and classes in the meantime.
-
-Other Information
-=================
-
-- N/A
diff --git a/source.json b/source.json
new file mode 100644
index 000000000..1b6967bb7
--- /dev/null
+++ b/source.json
@@ -0,0 +1,49 @@
+{
+ "name": "MeloNX",
+ "subtitle": "A source for the MeloNX Application",
+ "description": "Welcome to the MeloNX source! The latest download for MeloNX.",
+ "iconURL": "https://git.743378673.xyz/CycloKid/assets/media/branch/main/Melo/AppIcons/MeloNX.png",
+ "headerURL": "https://cdn.discordapp.com/attachments/1320760161836466257/1331670540447912090/melon-x-not-melo-nx-amiright-guys.png?ex=67f556d6&is=67f40556&hm=71be8f109a14f1c47d8f4965aa017bccb5617962b7a9f5cdfb936a5a8135dad7&",
+ "website": "https://MeloNX.org",
+ "tintColor": "#AE34EB",
+ "featuredApps": [
+ "com.stossy11.MeloNX"
+ ],
+ "apps": [
+ {
+ "name": "MeloNX",
+ "bundleIdentifier": "com.stossy11.MeloNX",
+ "developerName": "Stossy11",
+ "subtitle": "An NX Emulator.",
+ "localizedDescription": "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 a custom Gitea server under the MeloNX license (Based on MIT) (requires increased memory limit)",
+ "iconURL": "https://git.743378673.xyz/CycloKid/assets/media/branch/main/Melo/AppIcons/MeloNX.png",
+ "tintColor": "#AE34EB",
+ "category": "games",
+ "screenshots": [
+ "https://git.743378673.xyz/stossy11/screenshots/raw/branch/main/IMG_0380.PNG",
+ "https://git.743378673.xyz/stossy11/screenshots/raw/branch/main/IMG_0381.PNG"
+ ],
+ "versions": [
+ {
+ "version": "1.7.0",
+ "buildVersion": "1",
+ "date": "2025-04-08",
+ "localizedDescription": "First AltStore release!",
+ "downloadURL": "https://git.743378673.xyz/MeloNX/MeloNX/releases/download/1.7.0/MeloNX.ipa",
+ "size": 79821,
+ "minOSVersion": "15.0"
+ }
+ ],
+ "appPermissions": {
+ "entitlements": [
+ "get-task-allow",
+ "com.apple.developer.kernel.increased-memory-limit"
+ ],
+ "privacy": {
+ "NSPhotoLibraryAddUsageDescription": "MeloNX needs access to your Photo Library in order to save screenshots."
+ }
+ }
+ }
+ ],
+ "news": []
+}
diff --git a/src/ARMeilleure/CodeGen/Arm64/CodeGenContext.cs b/src/ARMeilleure/CodeGen/Arm64/CodeGenContext.cs
index 12ebabddd..89b1e9e6b 100644
--- a/src/ARMeilleure/CodeGen/Arm64/CodeGenContext.cs
+++ b/src/ARMeilleure/CodeGen/Arm64/CodeGenContext.cs
@@ -237,7 +237,7 @@ namespace ARMeilleure.CodeGen.Arm64
long originalPosition = _stream.Position;
_stream.Seek(0, SeekOrigin.Begin);
- _stream.Read(code, 0, code.Length);
+ _stream.ReadExactly(code, 0, code.Length);
_stream.Seek(originalPosition, SeekOrigin.Begin);
RelocInfo relocInfo;
diff --git a/src/ARMeilleure/CodeGen/RegisterAllocators/LinearScanAllocator.cs b/src/ARMeilleure/CodeGen/RegisterAllocators/LinearScanAllocator.cs
index f156e0886..16feeb914 100644
--- a/src/ARMeilleure/CodeGen/RegisterAllocators/LinearScanAllocator.cs
+++ b/src/ARMeilleure/CodeGen/RegisterAllocators/LinearScanAllocator.cs
@@ -251,7 +251,20 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
}
}
- int selectedReg = GetHighestValueIndex(freePositions);
+ // If this is a copy destination variable, we prefer the register used for the copy source.
+ // If the register is available, then the copy can be eliminated later as both source
+ // and destination will use the same register.
+ int selectedReg;
+
+ if (current.TryGetCopySourceRegister(out int preferredReg) && freePositions[preferredReg] >= current.GetEnd())
+ {
+ selectedReg = preferredReg;
+ }
+ else
+ {
+ selectedReg = GetHighestValueIndex(freePositions);
+ }
+
int selectedNextUse = freePositions[selectedReg];
// Intervals starts and ends at odd positions, unless they span an entire
@@ -431,7 +444,7 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
}
}
- private static int GetHighestValueIndex(Span span)
+ private static int GetHighestValueIndex(ReadOnlySpan span)
{
int highest = int.MinValue;
@@ -798,12 +811,12 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
// The "visited" state is stored in the MSB of the local's value.
const ulong VisitedMask = 1ul << 63;
- bool IsVisited(Operand local)
+ static bool IsVisited(Operand local)
{
return (local.GetValueUnsafe() & VisitedMask) != 0;
}
- void SetVisited(Operand local)
+ static void SetVisited(Operand local)
{
local.GetValueUnsafe() |= VisitedMask;
}
@@ -826,9 +839,25 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
{
dest.NumberLocal(_intervals.Count);
- _intervals.Add(new LiveInterval(dest));
+ LiveInterval interval = new LiveInterval(dest);
+ _intervals.Add(interval);
SetVisited(dest);
+
+ // If this is a copy (or copy-like operation), set the copy source interval as well.
+ // This is used for register preferencing later on, which allows the copy to be eliminated
+ // in some cases.
+ if (node.Instruction == Instruction.Copy || node.Instruction == Instruction.ZeroExtend32)
+ {
+ Operand source = node.GetSource(0);
+
+ if (source.Kind == OperandKind.LocalVariable &&
+ source.GetLocalNumber() > 0 &&
+ (node.Instruction == Instruction.Copy || source.Type == OperandType.I32))
+ {
+ interval.SetCopySource(_intervals[source.GetLocalNumber()]);
+ }
+ }
}
}
}
diff --git a/src/ARMeilleure/CodeGen/RegisterAllocators/LiveInterval.cs b/src/ARMeilleure/CodeGen/RegisterAllocators/LiveInterval.cs
index 333d3951b..cfe1bc7ca 100644
--- a/src/ARMeilleure/CodeGen/RegisterAllocators/LiveInterval.cs
+++ b/src/ARMeilleure/CodeGen/RegisterAllocators/LiveInterval.cs
@@ -19,6 +19,7 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
public LiveRange CurrRange;
public LiveInterval Parent;
+ public LiveInterval CopySource;
public UseList Uses;
public LiveIntervalList Children;
@@ -37,6 +38,7 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
private ref LiveRange CurrRange => ref _data->CurrRange;
private ref LiveRange PrevRange => ref _data->PrevRange;
private ref LiveInterval Parent => ref _data->Parent;
+ private ref LiveInterval CopySource => ref _data->CopySource;
private ref UseList Uses => ref _data->Uses;
private ref LiveIntervalList Children => ref _data->Children;
@@ -78,6 +80,25 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
Register = register;
}
+ public void SetCopySource(LiveInterval copySource)
+ {
+ CopySource = copySource;
+ }
+
+ public bool TryGetCopySourceRegister(out int copySourceRegIndex)
+ {
+ if (CopySource._data != null)
+ {
+ copySourceRegIndex = CopySource.Register.Index;
+
+ return true;
+ }
+
+ copySourceRegIndex = 0;
+
+ return false;
+ }
+
public void Reset()
{
PrevRange = default;
diff --git a/src/ARMeilleure/CodeGen/X86/Assembler.cs b/src/ARMeilleure/CodeGen/X86/Assembler.cs
index 55bf07248..96f4de049 100644
--- a/src/ARMeilleure/CodeGen/X86/Assembler.cs
+++ b/src/ARMeilleure/CodeGen/X86/Assembler.cs
@@ -1444,7 +1444,7 @@ namespace ARMeilleure.CodeGen.X86
Span buffer = new byte[jump.JumpPosition - _stream.Position];
- _stream.Read(buffer);
+ _stream.ReadExactly(buffer);
_stream.Seek(ReservedBytesForJump, SeekOrigin.Current);
codeStream.Write(buffer);
diff --git a/src/ARMeilleure/Decoders/OpCodeTable.cs b/src/ARMeilleure/Decoders/OpCodeTable.cs
index 9e13bd9b5..20d567fe5 100644
--- a/src/ARMeilleure/Decoders/OpCodeTable.cs
+++ b/src/ARMeilleure/Decoders/OpCodeTable.cs
@@ -517,7 +517,10 @@ namespace ARMeilleure.Decoders
SetA64("0x00111100>>>xxx100111xxxxxxxxxx", InstName.Sqrshrn_V, InstEmit.Sqrshrn_V, OpCodeSimdShImm.Create);
SetA64("0111111100>>>xxx100011xxxxxxxxxx", InstName.Sqrshrun_S, InstEmit.Sqrshrun_S, OpCodeSimdShImm.Create);
SetA64("0x10111100>>>xxx100011xxxxxxxxxx", InstName.Sqrshrun_V, InstEmit.Sqrshrun_V, OpCodeSimdShImm.Create);
+ SetA64("010111110>>>>xxx011101xxxxxxxxxx", InstName.Sqshl_Si, InstEmit.Sqshl_Si, OpCodeSimdShImm.Create);
SetA64("0>001110<<1xxxxx010011xxxxxxxxxx", InstName.Sqshl_V, InstEmit.Sqshl_V, OpCodeSimdReg.Create);
+ SetA64("0000111100>>>xxx011101xxxxxxxxxx", InstName.Sqshl_Vi, InstEmit.Sqshl_Vi, OpCodeSimdShImm.Create);
+ SetA64("010011110>>>>xxx011101xxxxxxxxxx", InstName.Sqshl_Vi, InstEmit.Sqshl_Vi, OpCodeSimdShImm.Create);
SetA64("0101111100>>>xxx100101xxxxxxxxxx", InstName.Sqshrn_S, InstEmit.Sqshrn_S, OpCodeSimdShImm.Create);
SetA64("0x00111100>>>xxx100101xxxxxxxxxx", InstName.Sqshrn_V, InstEmit.Sqshrn_V, OpCodeSimdShImm.Create);
SetA64("0111111100>>>xxx100001xxxxxxxxxx", InstName.Sqshrun_S, InstEmit.Sqshrun_S, OpCodeSimdShImm.Create);
@@ -743,6 +746,7 @@ namespace ARMeilleure.Decoders
SetA32("<<<<01101000xxxxxxxxxxxxxx01xxxx", InstName.Pkh, InstEmit32.Pkh, OpCode32AluRsImm.Create);
SetA32("11110101xx01xxxx1111xxxxxxxxxxxx", InstName.Pld, InstEmit32.Nop, OpCode32.Create);
SetA32("11110111xx01xxxx1111xxxxxxx0xxxx", InstName.Pld, InstEmit32.Nop, OpCode32.Create);
+ SetA32("<<<<01100010xxxxxxxx11110001xxxx", InstName.Qadd16, InstEmit32.Qadd16, OpCode32AluReg.Create);
SetA32("<<<<011011111111xxxx11110011xxxx", InstName.Rbit, InstEmit32.Rbit, OpCode32AluReg.Create);
SetA32("<<<<011010111111xxxx11110011xxxx", InstName.Rev, InstEmit32.Rev, OpCode32AluReg.Create);
SetA32("<<<<011010111111xxxx11111011xxxx", InstName.Rev16, InstEmit32.Rev16, OpCode32AluReg.Create);
@@ -819,6 +823,10 @@ namespace ARMeilleure.Decoders
SetA32("<<<<00000100xxxxxxxxxxxx1001xxxx", InstName.Umaal, InstEmit32.Umaal, OpCode32AluUmull.Create);
SetA32("<<<<0000101xxxxxxxxxxxxx1001xxxx", InstName.Umlal, InstEmit32.Umlal, OpCode32AluUmull.Create);
SetA32("<<<<0000100xxxxxxxxxxxxx1001xxxx", InstName.Umull, InstEmit32.Umull, OpCode32AluUmull.Create);
+ SetA32("<<<<01100110xxxxxxxx11110001xxxx", InstName.Uqadd16, InstEmit32.Uqadd16, OpCode32AluReg.Create);
+ SetA32("<<<<01100110xxxxxxxx11111001xxxx", InstName.Uqadd8, InstEmit32.Uqadd8, OpCode32AluReg.Create);
+ SetA32("<<<<01100110xxxxxxxx11110111xxxx", InstName.Uqsub16, InstEmit32.Uqsub16, OpCode32AluReg.Create);
+ SetA32("<<<<01100110xxxxxxxx11111111xxxx", InstName.Uqsub8, InstEmit32.Uqsub8, OpCode32AluReg.Create);
SetA32("<<<<0110111xxxxxxxxxxxxxxx01xxxx", InstName.Usat, InstEmit32.Usat, OpCode32Sat.Create);
SetA32("<<<<01101110xxxxxxxx11110011xxxx", InstName.Usat16, InstEmit32.Usat16, OpCode32Sat16.Create);
SetA32("<<<<01100101xxxxxxxx11111111xxxx", InstName.Usub8, InstEmit32.Usub8, OpCode32AluReg.Create);
@@ -872,6 +880,7 @@ namespace ARMeilleure.Decoders
SetVfp("<<<<11100x10xxxxxxxx101xx1x0xxxx", InstName.Vnmul, InstEmit32.Vnmul_S, OpCode32SimdRegS.Create, OpCode32SimdRegS.CreateT32);
SetVfp("111111101x1110xxxxxx101x01x0xxxx", InstName.Vrint, InstEmit32.Vrint_RM, OpCode32SimdS.Create, OpCode32SimdS.CreateT32);
SetVfp("<<<<11101x110110xxxx101x11x0xxxx", InstName.Vrint, InstEmit32.Vrint_Z, OpCode32SimdS.Create, OpCode32SimdS.CreateT32);
+ SetVfp("<<<<11101x110110xxxx101x01x0xxxx", InstName.Vrintr, InstEmit32.Vrintr_S, OpCode32SimdS.Create, OpCode32SimdS.CreateT32);
SetVfp("<<<<11101x110111xxxx101x01x0xxxx", InstName.Vrintx, InstEmit32.Vrintx_S, OpCode32SimdS.Create, OpCode32SimdS.CreateT32);
SetVfp("<<<<11101x110001xxxx101x11x0xxxx", InstName.Vsqrt, InstEmit32.Vsqrt_S, OpCode32SimdS.Create, OpCode32SimdS.CreateT32);
SetVfp("111111100xxxxxxxxxxx101xx0x0xxxx", InstName.Vsel, InstEmit32.Vsel, OpCode32SimdSel.Create, OpCode32SimdSel.CreateT32);
@@ -992,6 +1001,7 @@ namespace ARMeilleure.Decoders
SetAsimd("1111001x1x000xxxxxxx<>>xxxxxxx100101x1xxx0", InstName.Vqrshrn, InstEmit32.Vqrshrn, OpCode32SimdShImmNarrow.Create, OpCode32SimdShImmNarrow.CreateT32);
SetAsimd("111100111x>>>xxxxxxx100001x1xxx0", InstName.Vqrshrun, InstEmit32.Vqrshrun, OpCode32SimdShImmNarrow.Create, OpCode32SimdShImmNarrow.CreateT32);
SetAsimd("1111001x1x>>>xxxxxxx100100x1xxx0", InstName.Vqshrn, InstEmit32.Vqshrn, OpCode32SimdShImmNarrow.Create, OpCode32SimdShImmNarrow.CreateT32);
@@ -1023,8 +1035,10 @@ namespace ARMeilleure.Decoders
SetAsimd("111100101x>>>xxxxxxx0101>xx1xxxx", InstName.Vshl, InstEmit32.Vshl, OpCode32SimdShImm.Create, OpCode32SimdShImm.CreateT32);
SetAsimd("1111001x0xxxxxxxxxxx0100xxx0xxxx", InstName.Vshl, InstEmit32.Vshl_I, OpCode32SimdReg.Create, OpCode32SimdReg.CreateT32);
SetAsimd("1111001x1x>>>xxxxxxx101000x1xxxx", InstName.Vshll, InstEmit32.Vshll, OpCode32SimdShImmLong.Create, OpCode32SimdShImmLong.CreateT32); // A1 encoding.
+ SetAsimd("111100111x11<<10xxxx001100x0xxxx", InstName.Vshll, InstEmit32.Vshll2, OpCode32SimdMovn.Create, OpCode32SimdMovn.CreateT32); // A2 encoding.
SetAsimd("1111001x1x>>>xxxxxxx0000>xx1xxxx", InstName.Vshr, InstEmit32.Vshr, OpCode32SimdShImm.Create, OpCode32SimdShImm.CreateT32);
SetAsimd("111100101x>>>xxxxxxx100000x1xxx0", InstName.Vshrn, InstEmit32.Vshrn, OpCode32SimdShImmNarrow.Create, OpCode32SimdShImmNarrow.CreateT32);
+ SetAsimd("111100111x>>>xxxxxxx0101>xx1xxxx", InstName.Vsli, InstEmit32.Vsli_I, OpCode32SimdShImm.Create, OpCode32SimdShImm.CreateT32);
SetAsimd("1111001x1x>>>xxxxxxx0001>xx1xxxx", InstName.Vsra, InstEmit32.Vsra, OpCode32SimdShImm.Create, OpCode32SimdShImm.CreateT32);
SetAsimd("111101001x00xxxxxxxx0000xxx0xxxx", InstName.Vst1, InstEmit32.Vst1, OpCode32SimdMemSingle.Create, OpCode32SimdMemSingle.CreateT32);
SetAsimd("111101001x00xxxxxxxx0100xx0xxxxx", InstName.Vst1, InstEmit32.Vst1, OpCode32SimdMemSingle.Create, OpCode32SimdMemSingle.CreateT32);
@@ -1049,6 +1063,7 @@ namespace ARMeilleure.Decoders
SetAsimd("111100100x10xxxxxxxx1101xxx0xxxx", InstName.Vsub, InstEmit32.Vsub_V, OpCode32SimdReg.Create, OpCode32SimdReg.CreateT32);
SetAsimd("1111001x1x<
+ {
+ EmitSaturateRange(context, d, context.Add(n, m), 16, unsigned: false, setQ: false);
+ }));
+ }
+
public static void Rbit(ArmEmitterContext context)
{
Operand m = GetAluM(context);
@@ -467,6 +485,12 @@ namespace ARMeilleure.Instructions
Operand n = GetAluN(context);
Operand m = GetAluM(context, setCarry: false);
+ if (op.Rn == RegisterAlias.Aarch32Pc && op is OpCodeT32AluImm12)
+ {
+ // For ADR, PC is always 4 bytes aligned, even in Thumb mode.
+ n = context.BitwiseAnd(n, Const(~3u));
+ }
+
Operand res = context.Subtract(n, m);
if (ShouldSetFlags(context))
@@ -546,6 +570,46 @@ namespace ARMeilleure.Instructions
EmitHsub8(context, unsigned: true);
}
+ public static void Uqadd16(ArmEmitterContext context)
+ {
+ OpCode32AluReg op = (OpCode32AluReg)context.CurrOp;
+
+ SetIntA32(context, op.Rd, EmitUnsigned16BitPair(context, GetIntA32(context, op.Rn), GetIntA32(context, op.Rm), (d, n, m) =>
+ {
+ EmitSaturateUqadd(context, d, context.Add(n, m), 16);
+ }));
+ }
+
+ public static void Uqadd8(ArmEmitterContext context)
+ {
+ OpCode32AluReg op = (OpCode32AluReg)context.CurrOp;
+
+ SetIntA32(context, op.Rd, EmitUnsigned8BitPair(context, GetIntA32(context, op.Rn), GetIntA32(context, op.Rm), (d, n, m) =>
+ {
+ EmitSaturateUqadd(context, d, context.Add(n, m), 8);
+ }));
+ }
+
+ public static void Uqsub16(ArmEmitterContext context)
+ {
+ OpCode32AluReg op = (OpCode32AluReg)context.CurrOp;
+
+ SetIntA32(context, op.Rd, EmitUnsigned16BitPair(context, GetIntA32(context, op.Rn), GetIntA32(context, op.Rm), (d, n, m) =>
+ {
+ EmitSaturateUqsub(context, d, context.Subtract(n, m), 16);
+ }));
+ }
+
+ public static void Uqsub8(ArmEmitterContext context)
+ {
+ OpCode32AluReg op = (OpCode32AluReg)context.CurrOp;
+
+ SetIntA32(context, op.Rd, EmitUnsigned8BitPair(context, GetIntA32(context, op.Rn), GetIntA32(context, op.Rm), (d, n, m) =>
+ {
+ EmitSaturateUqsub(context, d, context.Subtract(n, m), 8);
+ }));
+ }
+
public static void Usat(ArmEmitterContext context)
{
OpCode32Sat op = (OpCode32Sat)context.CurrOp;
@@ -922,6 +986,251 @@ namespace ARMeilleure.Instructions
}
}
+ private static void EmitSaturateRange(ArmEmitterContext context, Operand result, Operand value, uint saturateTo, bool unsigned, bool setQ = true)
+ {
+ Debug.Assert(saturateTo <= 32);
+ Debug.Assert(!unsigned || saturateTo < 32);
+
+ if (!unsigned && saturateTo == 32)
+ {
+ // No saturation possible for this case.
+
+ context.Copy(result, value);
+
+ return;
+ }
+ else if (saturateTo == 0)
+ {
+ // Result is always zero if we saturate 0 bits.
+
+ context.Copy(result, Const(0));
+
+ return;
+ }
+
+ Operand satValue;
+
+ if (unsigned)
+ {
+ // Negative values always saturate (to zero).
+ // So we must always ignore the sign bit when masking, so that the truncated value will differ from the original one.
+
+ satValue = context.BitwiseAnd(value, Const((int)(uint.MaxValue >> (32 - (int)saturateTo))));
+ }
+ else
+ {
+ satValue = context.ShiftLeft(value, Const(32 - (int)saturateTo));
+ satValue = context.ShiftRightSI(satValue, Const(32 - (int)saturateTo));
+ }
+
+ // If the result is 0, the values are equal and we don't need saturation.
+ Operand lblNoSat = Label();
+ context.BranchIfFalse(lblNoSat, context.Subtract(value, satValue));
+
+ // Saturate and set Q flag.
+ if (unsigned)
+ {
+ if (saturateTo == 31)
+ {
+ // Only saturation case possible when going from 32 bits signed to 32 or 31 bits unsigned
+ // is when the signed input is negative, as all positive values are representable on a 31 bits range.
+
+ satValue = Const(0);
+ }
+ else
+ {
+ satValue = context.ShiftRightSI(value, Const(31));
+ satValue = context.BitwiseNot(satValue);
+ satValue = context.ShiftRightUI(satValue, Const(32 - (int)saturateTo));
+ }
+ }
+ else
+ {
+ if (saturateTo == 1)
+ {
+ satValue = context.ShiftRightSI(value, Const(31));
+ }
+ else
+ {
+ satValue = Const(uint.MaxValue >> (33 - (int)saturateTo));
+ satValue = context.BitwiseExclusiveOr(satValue, context.ShiftRightSI(value, Const(31)));
+ }
+ }
+
+ if (setQ)
+ {
+ SetFlag(context, PState.QFlag, Const(1));
+ }
+
+ context.Copy(result, satValue);
+
+ Operand lblExit = Label();
+ context.Branch(lblExit);
+
+ context.MarkLabel(lblNoSat);
+
+ context.Copy(result, value);
+
+ context.MarkLabel(lblExit);
+ }
+
+ private static void EmitSaturateUqadd(ArmEmitterContext context, Operand result, Operand value, uint saturateTo)
+ {
+ Debug.Assert(saturateTo <= 32);
+
+ if (saturateTo == 32)
+ {
+ // No saturation possible for this case.
+
+ context.Copy(result, value);
+
+ return;
+ }
+ else if (saturateTo == 0)
+ {
+ // Result is always zero if we saturate 0 bits.
+
+ context.Copy(result, Const(0));
+
+ return;
+ }
+
+ // If the result is 0, the values are equal and we don't need saturation.
+ Operand lblNoSat = Label();
+ context.BranchIfFalse(lblNoSat, context.ShiftRightUI(value, Const((int)saturateTo)));
+
+ // Saturate.
+ context.Copy(result, Const(uint.MaxValue >> (32 - (int)saturateTo)));
+
+ Operand lblExit = Label();
+ context.Branch(lblExit);
+
+ context.MarkLabel(lblNoSat);
+
+ context.Copy(result, value);
+
+ context.MarkLabel(lblExit);
+ }
+
+ private static void EmitSaturateUqsub(ArmEmitterContext context, Operand result, Operand value, uint saturateTo)
+ {
+ Debug.Assert(saturateTo <= 32);
+
+ if (saturateTo == 32)
+ {
+ // No saturation possible for this case.
+
+ context.Copy(result, value);
+
+ return;
+ }
+ else if (saturateTo == 0)
+ {
+ // Result is always zero if we saturate 0 bits.
+
+ context.Copy(result, Const(0));
+
+ return;
+ }
+
+ // If the result is 0, the values are equal and we don't need saturation.
+ Operand lblNoSat = Label();
+ context.BranchIf(lblNoSat, value, Const(0), Comparison.GreaterOrEqual);
+
+ // Saturate.
+ // Assumes that the value can only underflow, since this is only used for unsigned subtraction.
+ context.Copy(result, Const(0));
+
+ Operand lblExit = Label();
+ context.Branch(lblExit);
+
+ context.MarkLabel(lblNoSat);
+
+ context.Copy(result, value);
+
+ context.MarkLabel(lblExit);
+ }
+
+ private static Operand EmitSigned16BitPair(ArmEmitterContext context, Operand rn, Operand rm, Action elementAction)
+ {
+ Operand tempD = context.AllocateLocal(OperandType.I32);
+
+ Operand tempN = context.SignExtend16(OperandType.I32, rn);
+ Operand tempM = context.SignExtend16(OperandType.I32, rm);
+ elementAction(tempD, tempN, tempM);
+ Operand tempD2 = context.ZeroExtend16(OperandType.I32, tempD);
+
+ tempN = context.ShiftRightSI(rn, Const(16));
+ tempM = context.ShiftRightSI(rm, Const(16));
+ elementAction(tempD, tempN, tempM);
+ return context.BitwiseOr(tempD2, context.ShiftLeft(tempD, Const(16)));
+ }
+
+ private static Operand EmitUnsigned16BitPair(ArmEmitterContext context, Operand rn, Operand rm, Action elementAction)
+ {
+ Operand tempD = context.AllocateLocal(OperandType.I32);
+
+ Operand tempN = context.ZeroExtend16(OperandType.I32, rn);
+ Operand tempM = context.ZeroExtend16(OperandType.I32, rm);
+ elementAction(tempD, tempN, tempM);
+ Operand tempD2 = context.ZeroExtend16(OperandType.I32, tempD);
+
+ tempN = context.ShiftRightUI(rn, Const(16));
+ tempM = context.ShiftRightUI(rm, Const(16));
+ elementAction(tempD, tempN, tempM);
+ return context.BitwiseOr(tempD2, context.ShiftLeft(tempD, Const(16)));
+ }
+
+ private static Operand EmitSigned8BitPair(ArmEmitterContext context, Operand rn, Operand rm, Action elementAction)
+ {
+ return Emit8BitPair(context, rn, rm, elementAction, unsigned: false);
+ }
+
+ private static Operand EmitUnsigned8BitPair(ArmEmitterContext context, Operand rn, Operand rm, Action elementAction)
+ {
+ return Emit8BitPair(context, rn, rm, elementAction, unsigned: true);
+ }
+
+ private static Operand Emit8BitPair(ArmEmitterContext context, Operand rn, Operand rm, Action elementAction, bool unsigned)
+ {
+ Operand tempD = context.AllocateLocal(OperandType.I32);
+ Operand result = default;
+
+ for (int b = 0; b < 4; b++)
+ {
+ Operand nByte = b != 0 ? context.ShiftRightUI(rn, Const(b * 8)) : rn;
+ Operand mByte = b != 0 ? context.ShiftRightUI(rm, Const(b * 8)) : rm;
+
+ if (unsigned)
+ {
+ nByte = context.ZeroExtend8(OperandType.I32, nByte);
+ mByte = context.ZeroExtend8(OperandType.I32, mByte);
+ }
+ else
+ {
+ nByte = context.SignExtend8(OperandType.I32, nByte);
+ mByte = context.SignExtend8(OperandType.I32, mByte);
+ }
+
+ elementAction(tempD, nByte, mByte);
+
+ if (b == 0)
+ {
+ result = context.ZeroExtend8(OperandType.I32, tempD);
+ }
+ else if (b < 3)
+ {
+ result = context.BitwiseOr(result, context.ShiftLeft(context.ZeroExtend8(OperandType.I32, tempD), Const(b * 8)));
+ }
+ else
+ {
+ result = context.BitwiseOr(result, context.ShiftLeft(tempD, Const(24)));
+ }
+ }
+
+ return result;
+ }
+
private static void EmitAluStore(ArmEmitterContext context, Operand value)
{
IOpCode32Alu op = (IOpCode32Alu)context.CurrOp;
diff --git a/src/ARMeilleure/Instructions/InstEmitMemoryHelper.cs b/src/ARMeilleure/Instructions/InstEmitMemoryHelper.cs
index 5610b7749..ace6fe1ce 100644
--- a/src/ARMeilleure/Instructions/InstEmitMemoryHelper.cs
+++ b/src/ARMeilleure/Instructions/InstEmitMemoryHelper.cs
@@ -403,19 +403,25 @@ namespace ARMeilleure.Instructions
{
return EmitHostMappedPointer(context, address);
}
- else if (context.Memory.Type == MemoryManagerType.HostTracked)
+ else if (context.Memory.Type.IsHostTracked())
{
+ if (address.Type == OperandType.I32)
+ {
+ address = context.ZeroExtend32(OperandType.I64, address);
+ }
+
+ if (context.Memory.Type == MemoryManagerType.HostTracked)
+ {
+ Operand mask = Const(ulong.MaxValue >> (64 - context.Memory.AddressSpaceBits));
+ address = context.BitwiseAnd(address, mask);
+ }
+
Operand ptBase = !context.HasPtc
? Const(context.Memory.PageTablePointer.ToInt64())
: Const(context.Memory.PageTablePointer.ToInt64(), Ptc.PageTableSymbol);
Operand ptOffset = context.ShiftRightUI(address, Const(PageBits));
- if (ptOffset.Type == OperandType.I32)
- {
- ptOffset = context.ZeroExtend32(OperandType.I64, ptOffset);
- }
-
return context.Add(address, context.Load(OperandType.I64, context.Add(ptBase, context.ShiftLeft(ptOffset, Const(3)))));
}
diff --git a/src/ARMeilleure/Instructions/InstEmitSimdArithmetic.cs b/src/ARMeilleure/Instructions/InstEmitSimdArithmetic.cs
index 543aab023..13d9fac68 100644
--- a/src/ARMeilleure/Instructions/InstEmitSimdArithmetic.cs
+++ b/src/ARMeilleure/Instructions/InstEmitSimdArithmetic.cs
@@ -2426,7 +2426,11 @@ namespace ARMeilleure.Instructions
}
else if (Optimizations.FastFP && Optimizations.UseSse41 && sizeF == 0)
{
- Operand res = EmitSse41Round32Exp8OpF(context, context.AddIntrinsic(Intrinsic.X86Rsqrtss, GetVec(op.Rn)), scalar: true);
+ // RSQRTSS handles subnormals as zero, which differs from Arm, so we can't use it here.
+
+ Operand res = context.AddIntrinsic(Intrinsic.X86Sqrtss, GetVec(op.Rn));
+ res = context.AddIntrinsic(Intrinsic.X86Rcpss, res);
+ res = EmitSse41Round32Exp8OpF(context, res, scalar: true);
context.Copy(GetVec(op.Rd), context.VectorZeroUpper96(res));
}
@@ -2451,7 +2455,11 @@ namespace ARMeilleure.Instructions
}
else if (Optimizations.FastFP && Optimizations.UseSse41 && sizeF == 0)
{
- Operand res = EmitSse41Round32Exp8OpF(context, context.AddIntrinsic(Intrinsic.X86Rsqrtps, GetVec(op.Rn)), scalar: false);
+ // RSQRTPS handles subnormals as zero, which differs from Arm, so we can't use it here.
+
+ Operand res = context.AddIntrinsic(Intrinsic.X86Sqrtps, GetVec(op.Rn));
+ res = context.AddIntrinsic(Intrinsic.X86Rcpps, res);
+ res = EmitSse41Round32Exp8OpF(context, res, scalar: false);
if (op.RegisterSize == RegisterSize.Simd64)
{
diff --git a/src/ARMeilleure/Instructions/InstEmitSimdArithmetic32.cs b/src/ARMeilleure/Instructions/InstEmitSimdArithmetic32.cs
index 27608ebf8..c807fc858 100644
--- a/src/ARMeilleure/Instructions/InstEmitSimdArithmetic32.cs
+++ b/src/ARMeilleure/Instructions/InstEmitSimdArithmetic32.cs
@@ -1115,6 +1115,13 @@ namespace ARMeilleure.Instructions
}
}
+ public static void Vpadal(ArmEmitterContext context)
+ {
+ OpCode32Simd op = (OpCode32Simd)context.CurrOp;
+
+ EmitVectorPairwiseTernaryLongOpI32(context, (op1, op2, op3) => context.Add(context.Add(op1, op2), op3), op.Opc != 1);
+ }
+
public static void Vpaddl(ArmEmitterContext context)
{
OpCode32Simd op = (OpCode32Simd)context.CurrOp;
@@ -1239,6 +1246,33 @@ namespace ARMeilleure.Instructions
EmitVectorUnaryNarrowOp32(context, (op1) => EmitSatQ(context, op1, 8 << op.Size, signedSrc: true, signedDst: false), signed: true);
}
+ public static void Vqrdmulh(ArmEmitterContext context)
+ {
+ OpCode32SimdReg op = (OpCode32SimdReg)context.CurrOp;
+ int eSize = 8 << op.Size;
+
+ EmitVectorBinaryOpI32(context, (op1, op2) =>
+ {
+ if (op.Size == 2)
+ {
+ op1 = context.SignExtend32(OperandType.I64, op1);
+ op2 = context.SignExtend32(OperandType.I64, op2);
+ }
+
+ Operand res = context.Multiply(op1, op2);
+ res = context.Add(res, Const(res.Type, 1L << (eSize - 2)));
+ res = context.ShiftRightSI(res, Const(eSize - 1));
+ res = EmitSatQ(context, res, eSize, signedSrc: true, signedDst: true);
+
+ if (op.Size == 2)
+ {
+ res = context.ConvertI64ToI32(res);
+ }
+
+ return res;
+ }, signed: true);
+ }
+
public static void Vqsub(ArmEmitterContext context)
{
OpCode32SimdReg op = (OpCode32SimdReg)context.CurrOp;
diff --git a/src/ARMeilleure/Instructions/InstEmitSimdCvt32.cs b/src/ARMeilleure/Instructions/InstEmitSimdCvt32.cs
index 630e114c4..8eef6b14d 100644
--- a/src/ARMeilleure/Instructions/InstEmitSimdCvt32.cs
+++ b/src/ARMeilleure/Instructions/InstEmitSimdCvt32.cs
@@ -578,6 +578,22 @@ namespace ARMeilleure.Instructions
}
}
+ // VRINTR (floating-point).
+ public static void Vrintr_S(ArmEmitterContext context)
+ {
+ if (Optimizations.UseAdvSimd)
+ {
+ InstEmitSimdHelper32Arm64.EmitScalarUnaryOpF32(context, Intrinsic.Arm64FrintiS);
+ }
+ else
+ {
+ EmitScalarUnaryOpF32(context, (op1) =>
+ {
+ return EmitRoundByRMode(context, op1);
+ });
+ }
+ }
+
// VRINTZ (floating-point).
public static void Vrint_Z(ArmEmitterContext context)
{
diff --git a/src/ARMeilleure/Instructions/InstEmitSimdHelper32.cs b/src/ARMeilleure/Instructions/InstEmitSimdHelper32.cs
index c1c59b87b..2f021a1a1 100644
--- a/src/ARMeilleure/Instructions/InstEmitSimdHelper32.cs
+++ b/src/ARMeilleure/Instructions/InstEmitSimdHelper32.cs
@@ -673,6 +673,35 @@ namespace ARMeilleure.Instructions
context.Copy(GetVecA32(op.Qd), res);
}
+ public static void EmitVectorPairwiseTernaryLongOpI32(ArmEmitterContext context, Func3I emit, bool signed)
+ {
+ OpCode32Simd op = (OpCode32Simd)context.CurrOp;
+
+ int elems = op.GetBytesCount() >> op.Size;
+ int pairs = elems >> 1;
+
+ Operand res = GetVecA32(op.Qd);
+
+ for (int index = 0; index < pairs; index++)
+ {
+ int pairIndex = index * 2;
+ Operand m1 = EmitVectorExtract32(context, op.Qm, op.Im + pairIndex, op.Size, signed);
+ Operand m2 = EmitVectorExtract32(context, op.Qm, op.Im + pairIndex + 1, op.Size, signed);
+
+ if (op.Size == 2)
+ {
+ m1 = signed ? context.SignExtend32(OperandType.I64, m1) : context.ZeroExtend32(OperandType.I64, m1);
+ m2 = signed ? context.SignExtend32(OperandType.I64, m2) : context.ZeroExtend32(OperandType.I64, m2);
+ }
+
+ Operand d1 = EmitVectorExtract32(context, op.Qd, op.Id + index, op.Size + 1, signed);
+
+ res = EmitVectorInsert(context, res, emit(m1, m2, d1), op.Id + index, op.Size + 1);
+ }
+
+ context.Copy(GetVecA32(op.Qd), res);
+ }
+
// Narrow
public static void EmitVectorUnaryNarrowOp32(ArmEmitterContext context, Func1I emit, bool signed = false)
diff --git a/src/ARMeilleure/Instructions/InstEmitSimdMove32.cs b/src/ARMeilleure/Instructions/InstEmitSimdMove32.cs
index 9fa740997..fb2641f66 100644
--- a/src/ARMeilleure/Instructions/InstEmitSimdMove32.cs
+++ b/src/ARMeilleure/Instructions/InstEmitSimdMove32.cs
@@ -191,6 +191,26 @@ namespace ARMeilleure.Instructions
context.Copy(GetVecA32(op.Qd), res);
}
+ public static void Vswp(ArmEmitterContext context)
+ {
+ OpCode32Simd op = (OpCode32Simd)context.CurrOp;
+
+ if (op.Q)
+ {
+ Operand temp = context.Copy(GetVecA32(op.Qd));
+
+ context.Copy(GetVecA32(op.Qd), GetVecA32(op.Qm));
+ context.Copy(GetVecA32(op.Qm), temp);
+ }
+ else
+ {
+ Operand temp = ExtractScalar(context, OperandType.I64, op.Vd);
+
+ InsertScalar(context, op.Vd, ExtractScalar(context, OperandType.I64, op.Vm));
+ InsertScalar(context, op.Vm, temp);
+ }
+ }
+
public static void Vtbl(ArmEmitterContext context)
{
OpCode32SimdTbl op = (OpCode32SimdTbl)context.CurrOp;
diff --git a/src/ARMeilleure/Instructions/InstEmitSimdShift.cs b/src/ARMeilleure/Instructions/InstEmitSimdShift.cs
index be0670645..94e912579 100644
--- a/src/ARMeilleure/Instructions/InstEmitSimdShift.cs
+++ b/src/ARMeilleure/Instructions/InstEmitSimdShift.cs
@@ -116,7 +116,7 @@ namespace ARMeilleure.Instructions
}
else if (shift >= eSize)
{
- if ((op.RegisterSize == RegisterSize.Simd64))
+ if (op.RegisterSize == RegisterSize.Simd64)
{
Operand res = context.VectorZeroUpper64(GetVec(op.Rd));
@@ -359,6 +359,16 @@ namespace ARMeilleure.Instructions
}
}
+ public static void Sqshl_Si(ArmEmitterContext context)
+ {
+ EmitShlImmOp(context, signedDst: true, ShlRegFlags.Signed | ShlRegFlags.Scalar | ShlRegFlags.Saturating);
+ }
+
+ public static void Sqshl_Vi(ArmEmitterContext context)
+ {
+ EmitShlImmOp(context, signedDst: true, ShlRegFlags.Signed | ShlRegFlags.Saturating);
+ }
+
public static void Sqshrn_S(ArmEmitterContext context)
{
if (Optimizations.UseAdvSimd)
@@ -1593,6 +1603,99 @@ namespace ARMeilleure.Instructions
Saturating = 1 << 3,
}
+ private static void EmitShlImmOp(ArmEmitterContext context, bool signedDst, ShlRegFlags flags = ShlRegFlags.None)
+ {
+ bool scalar = flags.HasFlag(ShlRegFlags.Scalar);
+ bool signed = flags.HasFlag(ShlRegFlags.Signed);
+ bool saturating = flags.HasFlag(ShlRegFlags.Saturating);
+
+ OpCodeSimdShImm op = (OpCodeSimdShImm)context.CurrOp;
+
+ Operand res = context.VectorZero();
+
+ int elems = !scalar ? op.GetBytesCount() >> op.Size : 1;
+
+ for (int index = 0; index < elems; index++)
+ {
+ Operand ne = EmitVectorExtract(context, op.Rn, index, op.Size, signed);
+
+ Operand e = !saturating
+ ? EmitShlImm(context, ne, GetImmShl(op), op.Size)
+ : EmitShlImmSatQ(context, ne, GetImmShl(op), op.Size, signed, signedDst);
+
+ res = EmitVectorInsert(context, res, e, index, op.Size);
+ }
+
+ context.Copy(GetVec(op.Rd), res);
+ }
+
+ private static Operand EmitShlImm(ArmEmitterContext context, Operand op, int shiftLsB, int size)
+ {
+ int eSize = 8 << size;
+
+ Debug.Assert(op.Type == OperandType.I64);
+ Debug.Assert(eSize == 8 || eSize == 16 || eSize == 32 || eSize == 64);
+
+ Operand res = context.AllocateLocal(OperandType.I64);
+
+ if (shiftLsB >= eSize)
+ {
+ Operand shl = context.ShiftLeft(op, Const(shiftLsB));
+ context.Copy(res, shl);
+ }
+ else
+ {
+ Operand zeroL = Const(0L);
+ context.Copy(res, zeroL);
+ }
+
+ return res;
+ }
+
+ private static Operand EmitShlImmSatQ(ArmEmitterContext context, Operand op, int shiftLsB, int size, bool signedSrc, bool signedDst)
+ {
+ int eSize = 8 << size;
+
+ Debug.Assert(op.Type == OperandType.I64);
+ Debug.Assert(eSize == 8 || eSize == 16 || eSize == 32 || eSize == 64);
+
+ Operand lblEnd = Label();
+
+ Operand res = context.Copy(context.AllocateLocal(OperandType.I64), op);
+
+ if (shiftLsB >= eSize)
+ {
+ context.Copy(res, signedSrc
+ ? EmitSignedSignSatQ(context, op, size)
+ : EmitUnsignedSignSatQ(context, op, size));
+ }
+ else
+ {
+ Operand shl = context.ShiftLeft(op, Const(shiftLsB));
+ if (eSize == 64)
+ {
+ Operand sarOrShr = signedSrc
+ ? context.ShiftRightSI(shl, Const(shiftLsB))
+ : context.ShiftRightUI(shl, Const(shiftLsB));
+ context.Copy(res, shl);
+ context.BranchIf(lblEnd, sarOrShr, op, Comparison.Equal);
+ context.Copy(res, signedSrc
+ ? EmitSignedSignSatQ(context, op, size)
+ : EmitUnsignedSignSatQ(context, op, size));
+ }
+ else
+ {
+ context.Copy(res, signedSrc
+ ? EmitSignedSrcSatQ(context, shl, size, signedDst)
+ : EmitUnsignedSrcSatQ(context, shl, size, signedDst));
+ }
+ }
+
+ context.MarkLabel(lblEnd);
+
+ return res;
+ }
+
private static void EmitShlRegOp(ArmEmitterContext context, ShlRegFlags flags = ShlRegFlags.None)
{
bool scalar = flags.HasFlag(ShlRegFlags.Scalar);
diff --git a/src/ARMeilleure/Instructions/InstEmitSimdShift32.cs b/src/ARMeilleure/Instructions/InstEmitSimdShift32.cs
index e40600a47..eb28a0c5a 100644
--- a/src/ARMeilleure/Instructions/InstEmitSimdShift32.cs
+++ b/src/ARMeilleure/Instructions/InstEmitSimdShift32.cs
@@ -106,6 +106,38 @@ namespace ARMeilleure.Instructions
context.Copy(GetVecA32(op.Qd), res);
}
+ public static void Vshll2(ArmEmitterContext context)
+ {
+ OpCode32Simd op = (OpCode32Simd)context.CurrOp;
+
+ Operand res = context.VectorZero();
+
+ int elems = op.GetBytesCount() >> op.Size;
+
+ for (int index = 0; index < elems; index++)
+ {
+ Operand me = EmitVectorExtract32(context, op.Qm, op.Im + index, op.Size, !op.U);
+
+ if (op.Size == 2)
+ {
+ if (op.U)
+ {
+ me = context.ZeroExtend32(OperandType.I64, me);
+ }
+ else
+ {
+ me = context.SignExtend32(OperandType.I64, me);
+ }
+ }
+
+ me = context.ShiftLeft(me, Const(8 << op.Size));
+
+ res = EmitVectorInsert(context, res, me, index, op.Size + 1);
+ }
+
+ context.Copy(GetVecA32(op.Qd), res);
+ }
+
public static void Vshr(ArmEmitterContext context)
{
OpCode32SimdShImm op = (OpCode32SimdShImm)context.CurrOp;
@@ -130,6 +162,36 @@ namespace ARMeilleure.Instructions
EmitVectorUnaryNarrowOp32(context, (op1) => context.ShiftRightUI(op1, Const(shift)));
}
+ public static void Vsli_I(ArmEmitterContext context)
+ {
+ OpCode32SimdShImm op = (OpCode32SimdShImm)context.CurrOp;
+ int shift = op.Shift;
+ int eSize = 8 << op.Size;
+
+ ulong mask = shift != 0 ? ulong.MaxValue >> (64 - shift) : 0UL;
+
+ Operand res = GetVec(op.Qd);
+
+ int elems = op.GetBytesCount() >> op.Size;
+
+ for (int index = 0; index < elems; index++)
+ {
+ Operand me = EmitVectorExtractZx(context, op.Qm, op.Im + index, op.Size);
+
+ Operand neShifted = context.ShiftLeft(me, Const(shift));
+
+ Operand de = EmitVectorExtractZx(context, op.Qd, op.Id + index, op.Size);
+
+ Operand deMasked = context.BitwiseAnd(de, Const(mask));
+
+ Operand e = context.BitwiseOr(neShifted, deMasked);
+
+ res = EmitVectorInsert(context, res, e, op.Id + index, op.Size);
+ }
+
+ context.Copy(GetVec(op.Qd), res);
+ }
+
public static void Vsra(ArmEmitterContext context)
{
OpCode32SimdShImm op = (OpCode32SimdShImm)context.CurrOp;
diff --git a/src/ARMeilleure/Instructions/InstName.cs b/src/ARMeilleure/Instructions/InstName.cs
index 32ae38dad..74c33155b 100644
--- a/src/ARMeilleure/Instructions/InstName.cs
+++ b/src/ARMeilleure/Instructions/InstName.cs
@@ -384,7 +384,9 @@ namespace ARMeilleure.Instructions
Sqrshrn_V,
Sqrshrun_S,
Sqrshrun_V,
+ Sqshl_Si,
Sqshl_V,
+ Sqshl_Vi,
Sqshrn_S,
Sqshrn_V,
Sqshrun_S,
@@ -525,6 +527,7 @@ namespace ARMeilleure.Instructions
Pld,
Pop,
Push,
+ Qadd16,
Rev,
Revsh,
Rsb,
@@ -569,6 +572,10 @@ namespace ARMeilleure.Instructions
Umaal,
Umlal,
Umull,
+ Uqadd16,
+ Uqadd8,
+ Uqsub16,
+ Uqsub8,
Usat,
Usat16,
Usub8,
@@ -635,6 +642,7 @@ namespace ARMeilleure.Instructions
Vorn,
Vorr,
Vpadd,
+ Vpadal,
Vpaddl,
Vpmax,
Vpmin,
@@ -642,6 +650,7 @@ namespace ARMeilleure.Instructions
Vqdmulh,
Vqmovn,
Vqmovun,
+ Vqrdmulh,
Vqrshrn,
Vqrshrun,
Vqshrn,
@@ -654,6 +663,7 @@ namespace ARMeilleure.Instructions
Vrintm,
Vrintn,
Vrintp,
+ Vrintr,
Vrintx,
Vrshr,
Vrshrn,
@@ -662,6 +672,7 @@ namespace ARMeilleure.Instructions
Vshll,
Vshr,
Vshrn,
+ Vsli,
Vst1,
Vst2,
Vst3,
@@ -678,6 +689,7 @@ namespace ARMeilleure.Instructions
Vsub,
Vsubl,
Vsubw,
+ Vswp,
Vtbl,
Vtrn,
Vtst,
diff --git a/src/ARMeilleure/Instructions/NativeInterface.cs b/src/ARMeilleure/Instructions/NativeInterface.cs
index d1b2e353c..0cd3754f7 100644
--- a/src/ARMeilleure/Instructions/NativeInterface.cs
+++ b/src/ARMeilleure/Instructions/NativeInterface.cs
@@ -91,54 +91,54 @@ namespace ARMeilleure.Instructions
#region "Read"
public static byte ReadByte(ulong address)
{
- return GetMemoryManager().ReadTracked(address);
+ return GetMemoryManager().ReadGuest(address);
}
public static ushort ReadUInt16(ulong address)
{
- return GetMemoryManager().ReadTracked(address);
+ return GetMemoryManager().ReadGuest(address);
}
public static uint ReadUInt32(ulong address)
{
- return GetMemoryManager().ReadTracked(address);
+ return GetMemoryManager().ReadGuest(address);
}
public static ulong ReadUInt64(ulong address)
{
- return GetMemoryManager().ReadTracked(address);
+ return GetMemoryManager().ReadGuest(address);
}
public static V128 ReadVector128(ulong address)
{
- return GetMemoryManager().ReadTracked(address);
+ return GetMemoryManager().ReadGuest(address);
}
#endregion
#region "Write"
public static void WriteByte(ulong address, byte value)
{
- GetMemoryManager().Write(address, value);
+ GetMemoryManager().WriteGuest(address, value);
}
public static void WriteUInt16(ulong address, ushort value)
{
- GetMemoryManager().Write(address, value);
+ GetMemoryManager().WriteGuest(address, value);
}
public static void WriteUInt32(ulong address, uint value)
{
- GetMemoryManager().Write(address, value);
+ GetMemoryManager().WriteGuest(address, value);
}
public static void WriteUInt64(ulong address, ulong value)
{
- GetMemoryManager().Write(address, value);
+ GetMemoryManager().WriteGuest(address, value);
}
public static void WriteVector128(ulong address, V128 value)
{
- GetMemoryManager().Write(address, value);
+ GetMemoryManager().WriteGuest(address, value);
}
#endregion
diff --git a/src/ARMeilleure/Memory/IJitMemoryAllocator.cs b/src/ARMeilleure/Memory/IJitMemoryAllocator.cs
index 171bfd2f1..ff64bf13e 100644
--- a/src/ARMeilleure/Memory/IJitMemoryAllocator.cs
+++ b/src/ARMeilleure/Memory/IJitMemoryAllocator.cs
@@ -4,7 +4,5 @@ namespace ARMeilleure.Memory
{
IJitMemoryBlock Allocate(ulong size);
IJitMemoryBlock Reserve(ulong size);
-
- ulong GetPageSize();
}
}
diff --git a/src/ARMeilleure/Memory/IMemoryManager.cs b/src/ARMeilleure/Memory/IMemoryManager.cs
index 952cd2b4f..46d442655 100644
--- a/src/ARMeilleure/Memory/IMemoryManager.cs
+++ b/src/ARMeilleure/Memory/IMemoryManager.cs
@@ -28,6 +28,17 @@ namespace ARMeilleure.Memory
/// The data
T ReadTracked(ulong va) where T : unmanaged;
+ ///
+ /// Reads data from CPU mapped memory, from guest code. (with read tracking)
+ ///
+ /// Type of the data being read
+ /// Virtual address of the data in memory
+ /// The data
+ T ReadGuest(ulong va) where T : unmanaged
+ {
+ return ReadTracked(va);
+ }
+
///
/// Writes data to CPU mapped memory.
///
@@ -36,6 +47,17 @@ namespace ARMeilleure.Memory
/// Data to be written
void Write(ulong va, T value) where T : unmanaged;
+ ///
+ /// Writes data to CPU mapped memory, from guest code.
+ ///
+ /// Type of the data being written
+ /// Virtual address to write the data into
+ /// Data to be written
+ void WriteGuest(ulong va, T value) where T : unmanaged
+ {
+ Write(va, value);
+ }
+
///
/// Gets a read-only span of data from CPU mapped memory.
///
diff --git a/src/ARMeilleure/Memory/MemoryManagerType.cs b/src/ARMeilleure/Memory/MemoryManagerType.cs
index 757322b4b..290503837 100644
--- a/src/ARMeilleure/Memory/MemoryManagerType.cs
+++ b/src/ARMeilleure/Memory/MemoryManagerType.cs
@@ -35,18 +35,29 @@ namespace ARMeilleure.Memory
/// Allows invalid access from JIT code to the rest of the program, but is faster.
///
HostMappedUnsafe,
+
+ ///
+ /// High level implementation using a software flat page table for address translation
+ /// without masking the address and no support for handling invalid or non-contiguous memory access.
+ ///
+ HostTrackedUnsafe,
}
- static class MemoryManagerTypeExtensions
+ public static class MemoryManagerTypeExtensions
{
public static bool IsHostMapped(this MemoryManagerType type)
{
return type == MemoryManagerType.HostMapped || type == MemoryManagerType.HostMappedUnsafe;
}
+ public static bool IsHostTracked(this MemoryManagerType type)
+ {
+ return type == MemoryManagerType.HostTracked || type == MemoryManagerType.HostTrackedUnsafe;
+ }
+
public static bool IsHostMappedOrTracked(this MemoryManagerType type)
{
- return type == MemoryManagerType.HostTracked || type == MemoryManagerType.HostMapped || type == MemoryManagerType.HostMappedUnsafe;
+ return type.IsHostMapped() || type.IsHostTracked();
}
}
}
diff --git a/src/ARMeilleure/Signal/NativeSignalHandler.cs b/src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs
similarity index 62%
rename from src/ARMeilleure/Signal/NativeSignalHandler.cs
rename to src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs
index 40860a5d7..49c73ae3d 100644
--- a/src/ARMeilleure/Signal/NativeSignalHandler.cs
+++ b/src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs
@@ -1,63 +1,14 @@
-using ARMeilleure.IntermediateRepresentation;
-using ARMeilleure.Memory;
+using ARMeilleure.IntermediateRepresentation;
using ARMeilleure.Translation;
-using ARMeilleure.Translation.Cache;
using System;
-using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static ARMeilleure.IntermediateRepresentation.Operand.Factory;
namespace ARMeilleure.Signal
{
- [StructLayout(LayoutKind.Sequential, Pack = 1)]
- struct SignalHandlerRange
+ public static class NativeSignalHandlerGenerator
{
- public int IsActive;
- public nuint RangeAddress;
- public nuint RangeEndAddress;
- public IntPtr ActionPointer;
- }
-
- [StructLayout(LayoutKind.Sequential, Pack = 1)]
- struct SignalHandlerConfig
- {
- ///
- /// The byte offset of the faulting address in the SigInfo or ExceptionRecord struct.
- ///
- public int StructAddressOffset;
-
- ///
- /// The byte offset of the write flag in the SigInfo or ExceptionRecord struct.
- ///
- public int StructWriteOffset;
-
- ///
- /// The sigaction handler that was registered before this one. (unix only)
- ///
- public nuint UnixOldSigaction;
-
- ///
- /// The type of the previous sigaction. True for the 3 argument variant. (unix only)
- ///
- public int UnixOldSigaction3Arg;
-
- public SignalHandlerRange Range0;
- public SignalHandlerRange Range1;
- public SignalHandlerRange Range2;
- public SignalHandlerRange Range3;
- public SignalHandlerRange Range4;
- public SignalHandlerRange Range5;
- public SignalHandlerRange Range6;
- public SignalHandlerRange Range7;
- }
-
- public static class NativeSignalHandler
- {
- private delegate void UnixExceptionHandler(int sig, IntPtr info, IntPtr ucontext);
- [UnmanagedFunctionPointer(CallingConvention.Winapi)]
- private delegate int VectoredExceptionHandler(IntPtr exceptionInfo);
-
- private const int MaxTrackedRanges = 8;
+ public const int MaxTrackedRanges = 8;
private const int StructAddressOffset = 0;
private const int StructWriteOffset = 4;
@@ -70,124 +21,7 @@ namespace ARMeilleure.Signal
private const uint EXCEPTION_ACCESS_VIOLATION = 0xc0000005;
- private static ulong _pageSize;
- private static ulong _pageMask;
-
- private static readonly IntPtr _handlerConfig;
- private static IntPtr _signalHandlerPtr;
- private static IntPtr _signalHandlerHandle;
-
- private static readonly object _lock = new();
- private static bool _initialized;
-
- static NativeSignalHandler()
- {
- _handlerConfig = Marshal.AllocHGlobal(Unsafe.SizeOf());
- ref SignalHandlerConfig config = ref GetConfigRef();
-
- config = new SignalHandlerConfig();
- }
-
- public static void Initialize(IJitMemoryAllocator allocator)
- {
- JitCache.Initialize(allocator);
- }
-
- public static void InitializeSignalHandler(ulong pageSize, Func customSignalHandlerFactory = null)
- {
- if (_initialized)
- {
- return;
- }
-
- lock (_lock)
- {
- if (_initialized)
- {
- return;
- }
-
- _pageSize = pageSize;
- _pageMask = pageSize - 1;
-
- ref SignalHandlerConfig config = ref GetConfigRef();
-
- if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
- {
- _signalHandlerPtr = Marshal.GetFunctionPointerForDelegate(GenerateUnixSignalHandler(_handlerConfig));
-
- if (customSignalHandlerFactory != null)
- {
- _signalHandlerPtr = customSignalHandlerFactory(UnixSignalHandlerRegistration.GetSegfaultExceptionHandler().sa_handler, _signalHandlerPtr);
- }
-
- var old = UnixSignalHandlerRegistration.RegisterExceptionHandler(_signalHandlerPtr);
-
- config.UnixOldSigaction = (nuint)(ulong)old.sa_handler;
- config.UnixOldSigaction3Arg = old.sa_flags & 4;
- }
- else
- {
- config.StructAddressOffset = 40; // ExceptionInformation1
- config.StructWriteOffset = 32; // ExceptionInformation0
-
- _signalHandlerPtr = Marshal.GetFunctionPointerForDelegate(GenerateWindowsSignalHandler(_handlerConfig));
-
- if (customSignalHandlerFactory != null)
- {
- _signalHandlerPtr = customSignalHandlerFactory(IntPtr.Zero, _signalHandlerPtr);
- }
-
- _signalHandlerHandle = WindowsSignalHandlerRegistration.RegisterExceptionHandler(_signalHandlerPtr);
- }
-
- _initialized = true;
- }
- }
-
- private static unsafe ref SignalHandlerConfig GetConfigRef()
- {
- return ref Unsafe.AsRef((void*)_handlerConfig);
- }
-
- public static unsafe bool AddTrackedRegion(nuint address, nuint endAddress, IntPtr action)
- {
- var ranges = &((SignalHandlerConfig*)_handlerConfig)->Range0;
-
- for (int i = 0; i < MaxTrackedRanges; i++)
- {
- if (ranges[i].IsActive == 0)
- {
- ranges[i].RangeAddress = address;
- ranges[i].RangeEndAddress = endAddress;
- ranges[i].ActionPointer = action;
- ranges[i].IsActive = 1;
-
- return true;
- }
- }
-
- return false;
- }
-
- public static unsafe bool RemoveTrackedRegion(nuint address)
- {
- var ranges = &((SignalHandlerConfig*)_handlerConfig)->Range0;
-
- for (int i = 0; i < MaxTrackedRanges; i++)
- {
- if (ranges[i].IsActive == 1 && ranges[i].RangeAddress == address)
- {
- ranges[i].IsActive = 0;
-
- return true;
- }
- }
-
- return false;
- }
-
- private static Operand EmitGenericRegionCheck(EmitterContext context, IntPtr signalStructPtr, Operand faultAddress, Operand isWrite)
+ private static Operand EmitGenericRegionCheck(EmitterContext context, IntPtr signalStructPtr, Operand faultAddress, Operand isWrite, int rangeStructSize)
{
Operand inRegionLocal = context.AllocateLocal(OperandType.I32);
context.Copy(inRegionLocal, Const(0));
@@ -196,7 +30,7 @@ namespace ARMeilleure.Signal
for (int i = 0; i < MaxTrackedRanges; i++)
{
- ulong rangeBaseOffset = (ulong)(RangeOffset + i * Unsafe.SizeOf());
+ ulong rangeBaseOffset = (ulong)(RangeOffset + i * rangeStructSize);
Operand nextLabel = Label();
@@ -210,13 +44,12 @@ namespace ARMeilleure.Signal
// Is the fault address within this tracked region?
Operand inRange = context.BitwiseAnd(
context.ICompare(faultAddress, rangeAddress, Comparison.GreaterOrEqualUI),
- context.ICompare(faultAddress, rangeEndAddress, Comparison.LessUI)
- );
+ context.ICompare(faultAddress, rangeEndAddress, Comparison.LessUI));
// Only call tracking if in range.
context.BranchIfFalse(nextLabel, inRange, BasicBlockFrequency.Cold);
- Operand offset = context.BitwiseAnd(context.Subtract(faultAddress, rangeAddress), Const(~_pageMask));
+ Operand offset = context.Subtract(faultAddress, rangeAddress);
// Call the tracking action, with the pointer's relative offset to the base address.
Operand trackingActionPtr = context.Load(OperandType.I64, Const((ulong)signalStructPtr + rangeBaseOffset + 20));
@@ -227,8 +60,10 @@ namespace ARMeilleure.Signal
// Tracking action should be non-null to call it, otherwise assume false return.
context.BranchIfFalse(skipActionLabel, trackingActionPtr);
- Operand result = context.Call(trackingActionPtr, OperandType.I32, offset, Const(_pageSize), isWrite);
- context.Copy(inRegionLocal, result);
+ Operand result = context.Call(trackingActionPtr, OperandType.I64, offset, Const(1UL), isWrite);
+ context.Copy(inRegionLocal, context.ICompareNotEqual(result, Const(0UL)));
+
+ GenerateFaultAddressPatchCode(context, faultAddress, result);
context.MarkLabel(skipActionLabel);
@@ -269,8 +104,7 @@ namespace ARMeilleure.Signal
Operand esr = context.Load(OperandType.I64, context.Add(ctxPtr, Const(EsrOffset)));
return context.BitwiseAnd(esr, Const(0x40ul));
}
-
- if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
+ else if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
{
const ulong ErrOffset = 4; // __es.__err
Operand err = context.Load(OperandType.I64, context.Add(ctxPtr, Const(ErrOffset)));
@@ -310,8 +144,7 @@ namespace ARMeilleure.Signal
Operand esr = context.Load(OperandType.I64, context.Add(auxPtr, Const(8ul)));
return context.BitwiseAnd(esr, Const(0x40ul));
}
-
- if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
+ else if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
{
const int ErrOffset = 192; // uc_mcontext.gregs[REG_ERR]
Operand err = context.Load(OperandType.I64, context.Add(ucontextPtr, Const(ErrOffset)));
@@ -322,7 +155,7 @@ namespace ARMeilleure.Signal
throw new PlatformNotSupportedException();
}
- private static UnixExceptionHandler GenerateUnixSignalHandler(IntPtr signalStructPtr)
+ public static byte[] GenerateUnixSignalHandler(IntPtr signalStructPtr, int rangeStructSize)
{
EmitterContext context = new();
@@ -335,7 +168,7 @@ namespace ARMeilleure.Signal
Operand isWrite = context.ICompareNotEqual(writeFlag, Const(0L)); // Normalize to 0/1.
- Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite);
+ Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite, rangeStructSize);
Operand endLabel = Label();
@@ -367,10 +200,10 @@ namespace ARMeilleure.Signal
OperandType[] argTypes = new OperandType[] { OperandType.I32, OperandType.I64, OperandType.I64 };
- return Compiler.Compile(cfg, argTypes, OperandType.None, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map();
+ return Compiler.Compile(cfg, argTypes, OperandType.None, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Code;
}
- private static VectoredExceptionHandler GenerateWindowsSignalHandler(IntPtr signalStructPtr)
+ public static byte[] GenerateWindowsSignalHandler(IntPtr signalStructPtr, int rangeStructSize)
{
EmitterContext context = new();
@@ -399,7 +232,7 @@ namespace ARMeilleure.Signal
Operand isWrite = context.ICompareNotEqual(writeFlag, Const(0L)); // Normalize to 0/1.
- Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite);
+ Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite, rangeStructSize);
Operand endLabel = Label();
@@ -421,7 +254,88 @@ namespace ARMeilleure.Signal
OperandType[] argTypes = new OperandType[] { OperandType.I64 };
- return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map();
+ return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Code;
+ }
+
+ private static void GenerateFaultAddressPatchCode(EmitterContext context, Operand faultAddress, Operand newAddress)
+ {
+ if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
+ {
+ if (SupportsFaultAddressPatchingForHostOs())
+ {
+ Operand lblSkip = Label();
+
+ context.BranchIf(lblSkip, faultAddress, newAddress, Comparison.Equal);
+
+ Operand ucontextPtr = context.LoadArgument(OperandType.I64, 2);
+ Operand pcCtxAddress = default;
+ ulong baseRegsOffset = 0;
+
+ if (OperatingSystem.IsLinux())
+ {
+ pcCtxAddress = context.Add(ucontextPtr, Const(440UL));
+ baseRegsOffset = 184UL;
+ }
+ else if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
+ {
+ ucontextPtr = context.Load(OperandType.I64, context.Add(ucontextPtr, Const(48UL)));
+
+ pcCtxAddress = context.Add(ucontextPtr, Const(272UL));
+ baseRegsOffset = 16UL;
+ }
+
+ Operand pc = context.Load(OperandType.I64, pcCtxAddress);
+
+ Operand reg = GetAddressRegisterFromArm64Instruction(context, pc);
+ Operand reg64 = context.ZeroExtend32(OperandType.I64, reg);
+ Operand regCtxAddress = context.Add(ucontextPtr, context.Add(context.ShiftLeft(reg64, Const(3)), Const(baseRegsOffset)));
+ Operand regAddress = context.Load(OperandType.I64, regCtxAddress);
+
+ Operand addressDelta = context.Subtract(regAddress, faultAddress);
+
+ context.Store(regCtxAddress, context.Add(newAddress, addressDelta));
+
+ context.MarkLabel(lblSkip);
+ }
+ }
+ }
+
+ private static Operand GetAddressRegisterFromArm64Instruction(EmitterContext context, Operand pc)
+ {
+ Operand inst = context.Load(OperandType.I32, pc);
+ Operand reg = context.AllocateLocal(OperandType.I32);
+
+ Operand isSysInst = context.ICompareEqual(context.BitwiseAnd(inst, Const(0xFFF80000)), Const(0xD5080000));
+
+ Operand lblSys = Label();
+ Operand lblEnd = Label();
+
+ context.BranchIfTrue(lblSys, isSysInst, BasicBlockFrequency.Cold);
+
+ context.Copy(reg, context.BitwiseAnd(context.ShiftRightUI(inst, Const(5)), Const(0x1F)));
+ context.Branch(lblEnd);
+
+ context.MarkLabel(lblSys);
+ context.Copy(reg, context.BitwiseAnd(inst, Const(0x1F)));
+
+ context.MarkLabel(lblEnd);
+
+ return reg;
+ }
+
+ public static bool SupportsFaultAddressPatchingForHost()
+ {
+ return SupportsFaultAddressPatchingForHostArch() && SupportsFaultAddressPatchingForHostOs();
+ }
+
+ private static bool SupportsFaultAddressPatchingForHostArch()
+ {
+ return RuntimeInformation.ProcessArchitecture == Architecture.Arm64;
+ }
+
+ private static bool SupportsFaultAddressPatchingForHostOs()
+ {
+ return OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS();
}
}
}
diff --git a/src/ARMeilleure/Signal/WindowsPartialUnmapHandler.cs b/src/ARMeilleure/Signal/WindowsPartialUnmapHandler.cs
index 27a9ea83c..3bf6a4498 100644
--- a/src/ARMeilleure/Signal/WindowsPartialUnmapHandler.cs
+++ b/src/ARMeilleure/Signal/WindowsPartialUnmapHandler.cs
@@ -2,7 +2,7 @@ using ARMeilleure.IntermediateRepresentation;
using ARMeilleure.Translation;
using Ryujinx.Common.Memory.PartialUnmaps;
using System;
-
+using System.Runtime.InteropServices;
using static ARMeilleure.IntermediateRepresentation.Operand.Factory;
namespace ARMeilleure.Signal
@@ -10,8 +10,28 @@ namespace ARMeilleure.Signal
///
/// Methods to handle signals caused by partial unmaps. See the structs for C# implementations of the methods.
///
- internal static class WindowsPartialUnmapHandler
+ internal static partial class WindowsPartialUnmapHandler
{
+ [LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "LoadLibraryA")]
+ private static partial IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName);
+
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ private static partial IntPtr GetProcAddress(IntPtr hModule, [MarshalAs(UnmanagedType.LPStr)] string procName);
+
+ private static IntPtr _getCurrentThreadIdPtr;
+
+ public static IntPtr GetCurrentThreadIdFunc()
+ {
+ if (_getCurrentThreadIdPtr == IntPtr.Zero)
+ {
+ IntPtr handle = LoadLibrary("kernel32.dll");
+
+ _getCurrentThreadIdPtr = GetProcAddress(handle, "GetCurrentThreadId");
+ }
+
+ return _getCurrentThreadIdPtr;
+ }
+
public static Operand EmitRetryFromAccessViolation(EmitterContext context)
{
IntPtr partialRemapStatePtr = PartialUnmapState.GlobalState;
@@ -20,7 +40,7 @@ namespace ARMeilleure.Signal
// Get the lock first.
EmitNativeReaderLockAcquire(context, IntPtr.Add(partialRemapStatePtr, PartialUnmapState.PartialUnmapLockOffset));
- IntPtr getCurrentThreadId = WindowsSignalHandlerRegistration.GetCurrentThreadIdFunc();
+ IntPtr getCurrentThreadId = GetCurrentThreadIdFunc();
Operand threadId = context.Call(Const((ulong)getCurrentThreadId), OperandType.I32);
Operand threadIndex = EmitThreadLocalMapIntGetOrReserve(context, localCountsPtr, threadId, Const(0));
@@ -137,17 +157,6 @@ namespace ARMeilleure.Signal
return context.Add(structsPtr, context.SignExtend32(OperandType.I64, offset));
}
-#pragma warning disable IDE0051 // Remove unused private member
- private static void EmitThreadLocalMapIntRelease(EmitterContext context, IntPtr threadLocalMapPtr, Operand threadId, Operand index)
- {
- Operand offset = context.Multiply(index, Const(sizeof(int)));
- Operand idsPtr = Const((ulong)IntPtr.Add(threadLocalMapPtr, ThreadLocalMap.ThreadIdsOffset));
- Operand idPtr = context.Add(idsPtr, context.SignExtend32(OperandType.I64, offset));
-
- context.CompareAndSwap(idPtr, threadId, Const(0));
- }
-#pragma warning restore IDE0051
-
private static void EmitAtomicAddI32(EmitterContext context, Operand ptr, Operand additive)
{
Operand loop = Label();
diff --git a/src/ARMeilleure/Signal/WindowsSignalHandlerRegistration.cs b/src/ARMeilleure/Signal/WindowsSignalHandlerRegistration.cs
deleted file mode 100644
index 5444da0ca..000000000
--- a/src/ARMeilleure/Signal/WindowsSignalHandlerRegistration.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-
-namespace ARMeilleure.Signal
-{
- unsafe partial class WindowsSignalHandlerRegistration
- {
- [LibraryImport("kernel32.dll")]
- private static partial IntPtr AddVectoredExceptionHandler(uint first, IntPtr handler);
-
- [LibraryImport("kernel32.dll")]
- private static partial ulong RemoveVectoredExceptionHandler(IntPtr handle);
-
- [LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "LoadLibraryA")]
- private static partial IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName);
-
- [LibraryImport("kernel32.dll", SetLastError = true)]
- private static partial IntPtr GetProcAddress(IntPtr hModule, [MarshalAs(UnmanagedType.LPStr)] string procName);
-
- private static IntPtr _getCurrentThreadIdPtr;
-
- public static IntPtr RegisterExceptionHandler(IntPtr action)
- {
- return AddVectoredExceptionHandler(1, action);
- }
-
- public static bool RemoveExceptionHandler(IntPtr handle)
- {
- return RemoveVectoredExceptionHandler(handle) != 0;
- }
-
- public static IntPtr GetCurrentThreadIdFunc()
- {
- if (_getCurrentThreadIdPtr == IntPtr.Zero)
- {
- IntPtr handle = LoadLibrary("kernel32.dll");
-
- _getCurrentThreadIdPtr = GetProcAddress(handle, "GetCurrentThreadId");
- }
-
- return _getCurrentThreadIdPtr;
- }
- }
-}
diff --git a/src/ARMeilleure/Translation/Cache/JitCache.cs b/src/ARMeilleure/Translation/Cache/JitCache.cs
index 0da38b918..45f0688bf 100644
--- a/src/ARMeilleure/Translation/Cache/JitCache.cs
+++ b/src/ARMeilleure/Translation/Cache/JitCache.cs
@@ -25,7 +25,7 @@ namespace ARMeilleure.Translation.Cache
private static ReservedRegion _jitRegion;
private static JitCacheInvalidation _jitCacheInvalidator;
- private static CacheMemoryAllocator _cacheAllocator;
+ private static List _cacheAllocators = [];
private static readonly List _cacheEntries = new();
@@ -42,31 +42,42 @@ namespace ARMeilleure.Translation.Cache
public static void Initialize(IJitMemoryAllocator allocator)
{
- if (_initialized)
- {
- return;
- }
-
lock (_lock)
{
if (_initialized)
{
- return;
+ if (OperatingSystem.IsWindows())
+ {
+ // JitUnwindWindows.RemoveFunctionTableHandler(
+ // _jitRegions[0].Pointer);
+ }
+
+ for (int i = 0; i < _jitRegions.Count; i++)
+ {
+ _jitRegions[i].Dispose();
+ }
+
+ _jitRegions.Clear();
+ _cacheAllocators.Clear();
+ }
+ else
+ {
+ _initialized = true;
}
- var firstRegion = new ReservedRegion(allocator, CacheSize);
-
-
- _jitRegions.Add(firstRegion);
_activeRegionIndex = 0;
+ var firstRegion = new ReservedRegion(allocator, CacheSize);
+ _jitRegions.Add(firstRegion);
+
+ CacheMemoryAllocator firstCacheAllocator = new(CacheSize);
+ _cacheAllocators.Add(firstCacheAllocator);
+
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS())
{
_jitCacheInvalidator = new JitCacheInvalidation(allocator);
}
- _cacheAllocator = new CacheMemoryAllocator(CacheSize);
-
if (OperatingSystem.IsWindows())
{
JitUnwindWindows.InstallFunctionTableHandler(
@@ -106,8 +117,15 @@ namespace ARMeilleure.Translation.Cache
if (OperatingSystem.IsIOS())
{
Marshal.Copy(code, 0, funcPtr, code.Length);
- ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
- JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length);
+ if (deferProtect)
+ {
+ _deferredRxProtect.Enqueue((funcOffset, code.Length));
+ }
+ else
+ {
+ ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
+ JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length);
+ }
}
else if (OperatingSystem.IsMacOS()&& RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
@@ -162,7 +180,7 @@ namespace ARMeilleure.Translation.Cache
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{
- _cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
+ _cacheAllocators[_activeRegionIndex].Free(funcOffset, AlignCodeSize(entry.Size));
_cacheEntries.RemoveAt(entryIndex);
}
@@ -202,16 +220,12 @@ namespace ARMeilleure.Translation.Cache
alignment = 0x4000;
}
- for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
- {
- int allocOffset = _cacheAllocator.Allocate(ref codeSize, alignment);
+ int allocOffset = _cacheAllocators[_activeRegionIndex].Allocate(ref codeSize, alignment);
- if (allocOffset >= 0)
- {
- _jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
- _activeRegionIndex = i;
- return allocOffset;
- }
+ if (allocOffset >= 0)
+ {
+ _jitRegions[_activeRegionIndex].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
+ return allocOffset;
}
int exhaustedRegion = _activeRegionIndex;
@@ -221,9 +235,11 @@ namespace ARMeilleure.Translation.Cache
int newRegionNumber = _activeRegionIndex;
- _cacheAllocator = new CacheMemoryAllocator(CacheSize);
+ Logger.Info?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {_activeRegionIndex} ({((long)(_activeRegionIndex + 1) * CacheSize)} Total Allocation).");
- int allocOffsetNew = _cacheAllocator.Allocate(ref codeSize, alignment);
+ _cacheAllocators.Add(new CacheMemoryAllocator(CacheSize));
+
+ int allocOffsetNew = _cacheAllocators[_activeRegionIndex].Allocate(ref codeSize, alignment);
if (allocOffsetNew < 0)
{
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
diff --git a/src/ARMeilleure/Translation/ControlFlowGraph.cs b/src/ARMeilleure/Translation/ControlFlowGraph.cs
index 3ead49c93..45b092ec5 100644
--- a/src/ARMeilleure/Translation/ControlFlowGraph.cs
+++ b/src/ARMeilleure/Translation/ControlFlowGraph.cs
@@ -11,7 +11,7 @@ namespace ARMeilleure.Translation
private int[] _postOrderMap;
public int LocalsCount { get; private set; }
- public BasicBlock Entry { get; }
+ public BasicBlock Entry { get; private set; }
public IntrusiveList Blocks { get; }
public BasicBlock[] PostOrderBlocks => _postOrderBlocks;
public int[] PostOrderMap => _postOrderMap;
@@ -34,6 +34,15 @@ namespace ARMeilleure.Translation
return result;
}
+ public void UpdateEntry(BasicBlock newEntry)
+ {
+ newEntry.AddSuccessor(Entry);
+
+ Entry = newEntry;
+ Blocks.AddFirst(newEntry);
+ Update();
+ }
+
public void Update()
{
RemoveUnreachableBlocks(Blocks);
diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs
index 5ed27927a..a8383ee11 100644
--- a/src/ARMeilleure/Translation/PTC/Ptc.cs
+++ b/src/ARMeilleure/Translation/PTC/Ptc.cs
@@ -30,7 +30,7 @@ namespace ARMeilleure.Translation.PTC
private const string OuterHeaderMagicString = "PTCohd\0\0";
private const string InnerHeaderMagicString = "PTCihd\0\0";
- private const uint InternalVersion = 5518; //! To be incremented manually for each change to the ARMeilleure project.
+ private const uint InternalVersion = 6950; //! To be incremented manually for each change to the ARMeilleure project.
private const string ActualDir = "0";
private const string BackupDir = "1";
@@ -858,8 +858,14 @@ namespace ARMeilleure.Translation.PTC
Stopwatch sw = Stopwatch.StartNew();
- threads.ForEach((thread) => thread.Start());
- threads.ForEach((thread) => thread.Join());
+ foreach (var thread in threads)
+ {
+ thread.Start();
+ }
+ foreach (var thread in threads)
+ {
+ thread.Join();
+ }
threads.Clear();
diff --git a/src/ARMeilleure/Translation/RegisterUsage.cs b/src/ARMeilleure/Translation/RegisterUsage.cs
index c8c250626..472b0f67b 100644
--- a/src/ARMeilleure/Translation/RegisterUsage.cs
+++ b/src/ARMeilleure/Translation/RegisterUsage.cs
@@ -89,6 +89,17 @@ namespace ARMeilleure.Translation
public static void RunPass(ControlFlowGraph cfg, ExecutionMode mode)
{
+ if (cfg.Entry.Predecessors.Count != 0)
+ {
+ // We expect the entry block to have no predecessors.
+ // This is required because we have a implicit context load at the start of the function,
+ // but if there is a jump to the start of the function, the context load would trash the modified values.
+ // Here we insert a new entry block that will jump to the existing entry block.
+ BasicBlock newEntry = new BasicBlock(cfg.Blocks.Count);
+
+ cfg.UpdateEntry(newEntry);
+ }
+
// Compute local register inputs and outputs used inside blocks.
RegisterMask[] localInputs = new RegisterMask[cfg.Blocks.Count];
RegisterMask[] localOutputs = new RegisterMask[cfg.Blocks.Count];
@@ -201,7 +212,7 @@ namespace ARMeilleure.Translation
// The only block without any predecessor should be the entry block.
// It always needs a context load as it is the first block to run.
- if (block.Predecessors.Count == 0 || hasContextLoad)
+ if (block == cfg.Entry || hasContextLoad)
{
long vecMask = globalInputs[block.Index].VecMask;
long intMask = globalInputs[block.Index].IntMask;
diff --git a/src/ARMeilleure/Translation/Translator.cs b/src/ARMeilleure/Translation/Translator.cs
index 253f25e4a..9b28bed53 100644
--- a/src/ARMeilleure/Translation/Translator.cs
+++ b/src/ARMeilleure/Translation/Translator.cs
@@ -57,9 +57,6 @@ namespace ARMeilleure.Translation
private Thread[] _backgroundTranslationThreads;
private volatile int _threadCount;
- // FIXME: Remove this once the init logic of the emulator will be redone.
- public static readonly ManualResetEvent IsReadyForTranslation = new(false);
-
public Translator(IJitMemoryAllocator allocator, IMemoryManager memory, bool for64Bits)
{
_allocator = allocator;
@@ -79,11 +76,6 @@ namespace ARMeilleure.Translation
Stubs = new TranslatorStubs(FunctionTable);
FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub;
-
- if (memory.Type.IsHostMappedOrTracked())
- {
- NativeSignalHandler.InitializeSignalHandler(allocator.GetPageSize());
- }
}
public IPtcLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled)
@@ -105,8 +97,6 @@ namespace ARMeilleure.Translation
{
if (Interlocked.Increment(ref _threadCount) == 1)
{
- IsReadyForTranslation.WaitOne();
-
if (_ptc.State == PtcState.Enabled)
{
Debug.Assert(Functions.Count == 0);
diff --git a/src/ARMeilleure/Translation/TranslatorQueue.cs b/src/ARMeilleure/Translation/TranslatorQueue.cs
index cee2f9080..831522bc1 100644
--- a/src/ARMeilleure/Translation/TranslatorQueue.cs
+++ b/src/ARMeilleure/Translation/TranslatorQueue.cs
@@ -80,7 +80,10 @@ namespace ARMeilleure.Translation
return true;
}
- Monitor.Wait(Sync);
+ if (!_disposed)
+ {
+ Monitor.Wait(Sync);
+ }
}
}
diff --git a/src/MeloNX/MeloNX.xcconfig b/src/MeloNX/MeloNX.xcconfig
new file mode 100644
index 000000000..2adb67484
--- /dev/null
+++ b/src/MeloNX/MeloNX.xcconfig
@@ -0,0 +1,11 @@
+//
+// MeloNX.xcconfig
+// MeloNX
+//
+// Created by Stossy11 on 06/03/2025.
+//
+
+// Configuration settings file format documentation can be found at:
+// https://help.apple.com/xcode/#/dev745c5c974
+
+VERSION = 2.0.1
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj
index 567e8252e..b4c4648c6 100644
--- a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj
+++ b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj
@@ -24,7 +24,8 @@
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
- 4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */ = {isa = PBXBuildFile; productRef = 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */; };
+ 4549A31C2DD8795900EC8D88 /* CocoaAsyncSocket in Frameworks */ = {isa = PBXBuildFile; productRef = 4549A31B2DD8795900EC8D88 /* CocoaAsyncSocket */; };
+ 4E12B23C2D797CFA00FB2271 /* MeloNX.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */; };
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; };
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
@@ -45,6 +46,13 @@
remoteGlobalIDString = 4E80A98C2CD6F54500029585;
remoteInfo = MeloNX;
};
+ 4EFFCD182DFB766F00F78EA6 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = BD43C6212D1B248D003BBC42;
+ remoteInfo = com.Stossy11.MeloNX.RyujinxAg;
+ };
BD43C6252D1B249E003BBC42 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
@@ -78,12 +86,12 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = MeloNX.xcconfig; sourceTree = ""; };
4E7023A52D5A98E2002C7183 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
4E80A98D2CD6F54500029585 /* MeloNX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MeloNX.app; sourceTree = BUILT_PRODUCTS_DIR; };
4E80A99D2CD6F54700029585 /* MeloNXTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeloNXTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
4E80A9A72CD6F54700029585 /* MeloNXUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeloNXUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
4E80AA622CD7122800029585 /* GameController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameController.framework; path = System/Library/Frameworks/GameController.framework; sourceTree = SDKROOT; };
- 5650564A2D2A758600C8BB1E /* dotnet.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = dotnet.xcconfig; sourceTree = ""; };
BD43C6282D1B2514003BBC42 /* Ryujinx.Headless.SDL2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = Ryujinx.Headless.SDL2.dylib; path = "MeloNX/Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib"; sourceTree = ""; };
/* End PBXFileReference section */
@@ -108,7 +116,11 @@
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib" = (
CodeSignOnCopy,
);
- "Dependencies/Dynamic Libraries/RyujinxKeyboard.framework" = (
+ "Dependencies/Dynamic Libraries/RyujinxHelper.framework" = (
+ CodeSignOnCopy,
+ RemoveHeadersOnCopy,
+ );
+ "Dependencies/Dynamic Libraries/StosJIT.framework" = (
CodeSignOnCopy,
RemoveHeadersOnCopy,
);
@@ -121,10 +133,6 @@
"Dependencies/Dynamic Libraries/libavutil.dylib" = (
CodeSignOnCopy,
);
- Dependencies/XCFrameworks/MoltenVK.xcframework = (
- CodeSignOnCopy,
- RemoveHeadersOnCopy,
- );
Dependencies/XCFrameworks/SDL2.xcframework = (
CodeSignOnCopy,
RemoveHeadersOnCopy,
@@ -169,7 +177,8 @@
"Dependencies/Dynamic Libraries/libavutil.dylib",
"Dependencies/Dynamic Libraries/libMoltenVK.dylib",
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib",
- "Dependencies/Dynamic Libraries/RyujinxKeyboard.framework",
+ "Dependencies/Dynamic Libraries/RyujinxHelper.framework",
+ "Dependencies/Dynamic Libraries/StosJIT.framework",
Dependencies/XCFrameworks/libavcodec.xcframework,
Dependencies/XCFrameworks/libavfilter.xcframework,
Dependencies/XCFrameworks/libavformat.xcframework,
@@ -178,7 +187,6 @@
Dependencies/XCFrameworks/libswresample.xcframework,
Dependencies/XCFrameworks/libswscale.xcframework,
Dependencies/XCFrameworks/libteakra.xcframework,
- Dependencies/XCFrameworks/MoltenVK.xcframework,
Dependencies/XCFrameworks/SDL2.xcframework,
);
};
@@ -195,8 +203,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */,
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
+ 4549A31C2DD8795900EC8D88 /* CocoaAsyncSocket in Frameworks */,
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */,
);
@@ -222,7 +230,7 @@
4E80A9842CD6F54500029585 = {
isa = PBXGroup;
children = (
- 5650564A2D2A758600C8BB1E /* dotnet.xcconfig */,
+ 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */,
BD43C6282D1B2514003BBC42 /* Ryujinx.Headless.SDL2.dylib */,
4E80A98F2CD6F54500029585 /* MeloNX */,
4E80A9A02CD6F54700029585 /* MeloNXTests */,
@@ -256,12 +264,12 @@
/* Begin PBXLegacyTarget section */
BD43C61D2D1B23AB003BBC42 /* Ryujinx */ = {
isa = PBXLegacyTarget;
- buildArgumentsString = "publish -c Release -r ios-arm64 -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true";
+ buildArgumentsString = "./distribution/ios/xc-compile.sh";
buildConfigurationList = BD43C61E2D1B23AB003BBC42 /* Build configuration list for PBXLegacyTarget "Ryujinx" */;
buildPhases = (
);
- buildToolPath = /usr/local/share/dotnet/dotnet;
- buildWorkingDirectory = "$(SRCROOT)/../..";
+ buildToolPath = /bin/sh;
+ buildWorkingDirectory = "$(SRCROOT)/../../";
dependencies = (
);
name = Ryujinx;
@@ -286,14 +294,15 @@
buildRules = (
);
dependencies = (
+ 4EFFCD192DFB766F00F78EA6 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
4E80A98F2CD6F54500029585 /* MeloNX */,
);
name = MeloNX;
packageProductDependencies = (
- 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */,
4EA5AE812D16807500AD0B9F /* SwiftSVG */,
+ 4549A31B2DD8795900EC8D88 /* CocoaAsyncSocket */,
);
productName = MeloNX;
productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */;
@@ -353,7 +362,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1620;
- LastUpgradeCheck = 1610;
+ LastUpgradeCheck = 1620;
TargetAttributes = {
4E80A98C2CD6F54500029585 = {
CreatedOnToolsVersion = 16.1;
@@ -384,8 +393,8 @@
mainGroup = 4E80A9842CD6F54500029585;
minimizedProjectReferenceProxies = 1;
packageReferences = (
- 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */,
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */,
+ 4549A31A2DD8795900EC8D88 /* XCRemoteSwiftPackageReference "CocoaAsyncSocket" */,
);
preferredProjectObjectVersion = 56;
productRefGroup = 4E80A98E2CD6F54500029585 /* Products */;
@@ -406,6 +415,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 4E12B23C2D797CFA00FB2271 /* MeloNX.xcconfig in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -443,7 +453,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "cd ../..\nmv src/Ryujinx.Headless.SDL2/bin/Release/net8.0/ios-arm64/native/Ryujinx.Headless.SDL2.dylib src/MeloNX/MeloNX/Dependencies/Dynamic\\ Libraries/Ryujinx.Headless.SDL2.dylib\n";
+ shellScript = "cd ../..\nmv src/Ryujinx.Headless.SDL2/bin/Release/net8.0/ios-arm64/publish/Ryujinx.Headless.SDL2.dylib src/MeloNX/MeloNX/Dependencies/Dynamic\\ Libraries/Ryujinx.Headless.SDL2.dylib\n";
};
/* End PBXShellScriptBuildPhase section */
@@ -482,6 +492,11 @@
target = 4E80A98C2CD6F54500029585 /* MeloNX */;
targetProxy = 4E80A9A82CD6F54700029585 /* PBXContainerItemProxy */;
};
+ 4EFFCD192DFB766F00F78EA6 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
+ targetProxy = 4EFFCD182DFB766F00F78EA6 /* PBXContainerItemProxy */;
+ };
BD43C6262D1B249E003BBC42 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = BD43C61D2D1B23AB003BBC42 /* Ryujinx */;
@@ -492,6 +507,7 @@
/* Begin XCBuildConfiguration section */
4E80A9AF2CD6F54700029585 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
@@ -554,6 +570,7 @@
ONLY_ACTIVE_ARCH = NO;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_ENFORCE_EXCLUSIVE_ACCESS = "debug-only";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
@@ -561,6 +578,7 @@
};
4E80A9B02CD6F54700029585 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
@@ -619,6 +637,7 @@
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_DISABLE_SAFETY_CHECKS = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_ENFORCE_EXCLUSIVE_ACCESS = "debug-only";
VALIDATE_PRODUCT = YES;
};
@@ -626,14 +645,18 @@
};
4E80A9B22CD6F54700029585 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
+ ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = PixelAppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = MeloNX/MeloNX.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8;
+ EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO;
ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -674,8 +697,102 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
- GCC_OPTIMIZATION_LEVEL = fast;
+ GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = MeloNX/Info.plist;
INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES;
@@ -686,10 +803,10 @@
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
- INFOPLIST_KEY_UIRequiresFullScreen = YES;
+ INFOPLIST_KEY_UIRequiresFullScreen = NO;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -761,10 +878,180 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
- MARKETING_VERSION = 1.3.0;
+ MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
SWIFT_VERSION = 5.0;
@@ -774,14 +1061,18 @@
};
4E80A9B32CD6F54700029585 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
+ ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = PixelAppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = MeloNX/MeloNX.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8;
+ EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO;
ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = (
@@ -822,8 +1113,102 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
- GCC_OPTIMIZATION_LEVEL = fast;
+ GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = MeloNX/Info.plist;
INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES;
@@ -834,10 +1219,10 @@
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
- INFOPLIST_KEY_UIRequiresFullScreen = YES;
+ INFOPLIST_KEY_UIRequiresFullScreen = NO;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -909,10 +1294,180 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
- MARKETING_VERSION = 1.3.0;
+ MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
SWIFT_VERSION = 5.0;
@@ -922,6 +1477,7 @@
};
4E80A9B52CD6F54700029585 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -941,6 +1497,7 @@
};
4E80A9B62CD6F54700029585 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -960,6 +1517,7 @@
};
4E80A9B82CD6F54700029585 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
@@ -977,6 +1535,7 @@
};
4E80A9B92CD6F54700029585 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
@@ -994,7 +1553,9 @@
};
BD43C61F2D1B23AB003BBC42 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
CODE_SIGN_STYLE = Automatic;
DEBUGGING_SYMBOLS = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
@@ -1010,7 +1571,9 @@
};
BD43C6202D1B23AB003BBC42 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
CODE_SIGN_STYLE = Automatic;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 95J8WZ4TN8;
@@ -1022,6 +1585,7 @@
};
BD43C6232D1B248D003BBC42 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 95J8WZ4TN8;
@@ -1031,6 +1595,7 @@
};
BD43C6242D1B248D003BBC42 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 95J8WZ4TN8;
@@ -1098,12 +1663,12 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
- 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */ = {
+ 4549A31A2DD8795900EC8D88 /* XCRemoteSwiftPackageReference "CocoaAsyncSocket" */ = {
isa = XCRemoteSwiftPackageReference;
- repositoryURL = "https://github.com/michael94ellis/SwiftUIJoystick";
+ repositoryURL = "https://github.com/robbiehanson/CocoaAsyncSocket";
requirement = {
kind = upToNextMajorVersion;
- minimumVersion = 1.5.0;
+ minimumVersion = 7.6.5;
};
};
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */ = {
@@ -1117,10 +1682,10 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
- 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */ = {
+ 4549A31B2DD8795900EC8D88 /* CocoaAsyncSocket */ = {
isa = XCSwiftPackageProductDependency;
- package = 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */;
- productName = SwiftUIJoystick;
+ package = 4549A31A2DD8795900EC8D88 /* XCRemoteSwiftPackageReference "CocoaAsyncSocket" */;
+ productName = CocoaAsyncSocket;
};
4EA5AE812D16807500AD0B9F /* SwiftSVG */ = {
isa = XCSwiftPackageProductDependency;
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index af8dd513e..cb3a468df 100644
--- a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -1,6 +1,15 @@
{
- "originHash" : "d611b071fbe94fdc9900a07a218340eab4ce2c3c7168bf6542f2830c0400a72b",
+ "originHash" : "b4a593815773c4e9eedb98cabe88f41620776314bffb6c39d5a41cb743e4d390",
"pins" : [
+ {
+ "identity" : "cocoaasyncsocket",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/robbiehanson/CocoaAsyncSocket",
+ "state" : {
+ "revision" : "dbdc00669c1ced63b27c3c5f052ee4d28f10150c",
+ "version" : "7.6.5"
+ }
+ },
{
"identity" : "swiftsvg",
"kind" : "remoteSourceControl",
@@ -9,15 +18,6 @@
"branch" : "master",
"revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d"
}
- },
- {
- "identity" : "swiftuijoystick",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/michael94ellis/SwiftUIJoystick",
- "state" : {
- "revision" : "5bd303cdafb369a70a45c902538b42dd3c5f4d65",
- "version" : "1.5.0"
- }
}
],
"version" : 3
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/benlawrence.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/benlawrence.xcuserdatad/UserInterfaceState.xcuserstate
deleted file mode 100644
index d95c76d65..000000000
Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/benlawrence.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/brandon.xcuserdatad/IDEFindNavigatorScopes.plist b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/brandon.xcuserdatad/IDEFindNavigatorScopes.plist
deleted file mode 100644
index 5dd5da85f..000000000
--- a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/brandon.xcuserdatad/IDEFindNavigatorScopes.plist
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/brandon.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/brandon.xcuserdatad/UserInterfaceState.xcuserstate
deleted file mode 100644
index 77a8ba399..000000000
Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/brandon.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/june.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/june.xcuserdatad/UserInterfaceState.xcuserstate
deleted file mode 100644
index ebc8d0112..000000000
Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/june.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/ls.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/ls.xcuserdatad/UserInterfaceState.xcuserstate
deleted file mode 100644
index d2a61f079..000000000
Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/ls.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate
index 960d30a0e..b4c024fa9 100644
Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate and b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/techguy.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/techguy.xcuserdatad/UserInterfaceState.xcuserstate
deleted file mode 100644
index f234e2cfe..000000000
Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/techguy.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ
diff --git a/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/MeloNX.xcscheme b/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/MeloNX.xcscheme
index b48bf4f6f..f79f4ed54 100644
--- a/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/MeloNX.xcscheme
+++ b/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/MeloNX.xcscheme
@@ -1,7 +1,7 @@
+ LastUpgradeVersion = "1620"
+ version = "2.0">
+ allowLocationSimulation = "YES"
+ queueDebuggingEnabled = "No"
+ consoleMode = "0"
+ structuredConsoleMode = "2"
+ disablePerformanceAntipatternChecker = "YES">
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/Ryujinx.xcscheme b/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/Ryujinx.xcscheme
new file mode 100644
index 000000000..a5e708fe3
--- /dev/null
+++ b/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/Ryujinx.xcscheme
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MeloNX/MeloNX/App/Core/Entitlements/EntitlementChecker.swift b/src/MeloNX/MeloNX/App/Core/Entitlements/EntitlementChecker.swift
index 9c9d41ad8..49ddf5b07 100644
--- a/src/MeloNX/MeloNX/App/Core/Entitlements/EntitlementChecker.swift
+++ b/src/MeloNX/MeloNX/App/Core/Entitlements/EntitlementChecker.swift
@@ -17,11 +17,20 @@ func SecTaskCopyValueForEntitlement(
_ error: NSErrorPointer
) -> CFTypeRef?
+@_silgen_name("SecTaskCopyTeamIdentifier")
+func SecTaskCopyTeamIdentifier(
+ _ task: SecTaskRef,
+ _ error: NSErrorPointer
+) -> NSString?
+
@_silgen_name("SecTaskCreateFromSelf")
func SecTaskCreateFromSelf(
_ allocator: CFAllocator?
) -> SecTaskRef?
+@_silgen_name("CFRelease")
+func CFRelease(_ cf: CFTypeRef)
+
@_silgen_name("SecTaskCopyValuesForEntitlements")
func SecTaskCopyValuesForEntitlements(
_ task: SecTaskRef,
@@ -29,30 +38,43 @@ func SecTaskCopyValuesForEntitlements(
_ error: UnsafeMutablePointer?>?
) -> CFDictionary?
+func releaseSecTask(_ task: SecTaskRef) {
+ let cf = unsafeBitCast(task, to: CFTypeRef.self)
+ CFRelease(cf)
+}
+
func checkAppEntitlements(_ ents: [String]) -> [String: Any] {
guard let task = SecTaskCreateFromSelf(nil) else {
- print("Failed to create SecTask")
return [:]
}
-
+ defer {
+ releaseSecTask(task)
+ }
+
guard let entitlements = SecTaskCopyValuesForEntitlements(task, ents as CFArray, nil) else {
- print("Failed to get entitlements")
return [:]
}
-
- return (entitlements as? [String: Any]) ?? [:]
+
+ return (entitlements as NSDictionary) as? [String: Any] ?? [:]
}
func checkAppEntitlement(_ ent: String) -> Bool {
guard let task = SecTaskCreateFromSelf(nil) else {
- print("Failed to create SecTask")
return false
}
-
- guard let entitlements = SecTaskCopyValueForEntitlement(task, ent as NSString, nil) else {
- print("Failed to get entitlements")
+ defer {
+ releaseSecTask(task)
+ }
+
+ guard let entitlement = SecTaskCopyValueForEntitlement(task, ent as NSString, nil) else {
return false
}
-
- return entitlements.boolValue != nil && entitlements.boolValue
+
+ if let number = entitlement as? NSNumber {
+ return number.boolValue
+ } else if let bool = entitlement as? Bool {
+ return bool
+ }
+
+ return false
}
diff --git a/src/MeloNX/MeloNX/App/Core/Headers/Ryujinx-Header.h b/src/MeloNX/MeloNX/App/Core/Headers/Ryujinx-Header.h
index 3871afe5f..1a1b194f1 100644
--- a/src/MeloNX/MeloNX/App/Core/Headers/Ryujinx-Header.h
+++ b/src/MeloNX/MeloNX/App/Core/Headers/Ryujinx-Header.h
@@ -14,11 +14,14 @@
#include
#include
+#include
+
#ifdef __cplusplus
extern "C" {
#endif
+
struct GameInfo {
long FileSize;
char TitleName[512];
@@ -40,6 +43,10 @@ struct DlcNcaList {
struct DlcNcaListItem* items;
};
+typedef void (^SwiftCallback)(NSString *result);
+
+void RegisterCallback(NSString *identifier, SwiftCallback callback);
+
extern struct GameInfo get_game_info(int, char*);
extern struct DlcNcaList get_dlc_nca_list(const char* titleIdPtr, const char* pathPtr);
@@ -48,13 +55,25 @@ void install_firmware(const char* inputPtr);
char* installed_firmware_version();
+void set_native_window(void *layerPtr);
+
+void pause_emulation(bool shouldPause);
+
void stop_emulation();
+void initialize();
+
int main_ryujinx_sdl(int argc, char **argv);
+int update_settings_external(int argc, char **argv);
+
int get_current_fps();
-void initialize();
+void touch_began(float x, float y, int index);
+
+void touch_moved(float x, float y, int index);
+
+void touch_ended(int index);
#ifdef __cplusplus
}
diff --git a/src/MeloNX/MeloNX/App/Core/JIT/IsJITEnabled.swift b/src/MeloNX/MeloNX/App/Core/JIT/IsJITEnabled.swift
index 29c614a7c..fd5b7c68e 100644
--- a/src/MeloNX/MeloNX/App/Core/JIT/IsJITEnabled.swift
+++ b/src/MeloNX/MeloNX/App/Core/JIT/IsJITEnabled.swift
@@ -7,6 +7,27 @@
import Foundation
+@_silgen_name("csops")
+func csops(pid: Int32, ops: Int32, useraddr: UnsafeMutableRawPointer?, usersize: Int32) -> Int32
+
+func isJITEnabled() -> Bool {
+ var flags: Int = 0
+
+ if checkAppEntitlement("dynamic-codesigning") {
+ return allocateTest()
+ }
+
+ return csops(pid: getpid(), ops: 0, useraddr: &flags, usersize: Int32(MemoryLayout.size(ofValue: flags))) == 0 && (flags & Int(CS_DEBUGGED)) != 0 ? allocateTest() : false
+}
+
+func checkDebugged() -> Bool {
+ var flags: Int = 0
+ if checkAppEntitlement("dynamic-codesigning") {
+ return true
+ }
+ return csops(pid: getpid(), ops: 0, useraddr: &flags, usersize: Int32(MemoryLayout.size(ofValue: flags))) == 0 && (flags & Int(CS_DEBUGGED)) != 0
+}
+
func checkMemoryPermissions(at address: UnsafeRawPointer) -> Bool {
var region: vm_address_t = vm_address_t(UInt(bitPattern: address))
var regionSize: vm_size_t = 0
@@ -21,14 +42,13 @@ func checkMemoryPermissions(at address: UnsafeRawPointer) -> Bool {
}
if result != KERN_SUCCESS {
- print("Failed to reach \(address)")
+ // print("Failed to reach \(address)")
return false
}
return info.protection & VM_PROT_EXECUTE != 0
}
-
-func isJITEnabled() -> Bool {
+func allocateTest() -> Bool {
let pageSize = sysconf(_SC_PAGESIZE)
let code: [UInt32] = [0x52800540, 0xD65F03C0]
@@ -40,7 +60,6 @@ func isJITEnabled() -> Bool {
munmap(jitMemory, pageSize)
}
-
memcpy(jitMemory, code, code.count)
if mprotect(jitMemory, pageSize, PROT_READ | PROT_EXEC) != 0 {
diff --git a/src/MeloNX/MeloNX/App/Core/JIT/JitStreamerEB/EnableJIT.swift b/src/MeloNX/MeloNX/App/Core/JIT/JitStreamerEB/EnableJIT.swift
index 6aefdd297..d1eecfda4 100644
--- a/src/MeloNX/MeloNX/App/Core/JIT/JitStreamerEB/EnableJIT.swift
+++ b/src/MeloNX/MeloNX/App/Core/JIT/JitStreamerEB/EnableJIT.swift
@@ -6,38 +6,122 @@
//
import Foundation
+import Network
+import UIKit
-func enableJITEB() {
- guard let bundleID = Bundle.main.bundleIdentifier else {
- return
+func enableJITEB() {
+ if UserDefaults.standard.bool(forKey: "waitForVPN") {
+ waitForVPNConnection { connected in
+ if connected {
+ enableJITEBRequest()
+ }
+ }
+ } else {
+ enableJITEBRequest()
}
- let address = URL(string: "http://[fd00::]:9172/launch_app/\(bundleID)")!
+}
+
+func enableJITEBRequest() {
+ let pid = Int(getpid())
+ // print(pid)
- let task = URLSession.shared.dataTask(with: address) { data, response, error in
- if error != nil {
+ let address = URL(string: "http://[fd00::]:9172/attach/\(pid)")!
+ var request = URLRequest(url: address)
+ request.httpMethod = "POST"
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+
+ let task = URLSession.shared.dataTask(with: request) { data, response, error in
+ if let error = error {
+ presentAlert(title: "Request Error", message: error.localizedDescription)
return
}
-
- guard let httpResponse = response as? HTTPURLResponse else {
- return
- }
DispatchQueue.main.async {
- showLaunchAppAlert(jsonData: data!, in: UIApplication.shared.windows.last!.rootViewController!)
+ if let data = data, let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
+ showLaunchAppAlert(jsonData: data, in: windowScene.windows.last!.rootViewController!)
+ } else {
+ fatalError("Unable to get Window")
+ }
}
-
- return
}
task.resume()
}
+func waitForVPNConnection(timeout: TimeInterval = 30, interval: TimeInterval = 1, _ completion: @escaping (Bool) -> Void) {
+ let startTime = Date()
+ let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .background))
+
+ timer.schedule(deadline: .now(), repeating: interval)
+
+ timer.setEventHandler {
+ pingSite { connected in
+ if connected {
+ timer.cancel()
+ DispatchQueue.main.async {
+ completion(true)
+ }
+ } else if Date().timeIntervalSince(startTime) > timeout {
+ timer.cancel()
+ DispatchQueue.main.async {
+ completion(false)
+ }
+ }
+ }
+ }
+
+ timer.resume()
+}
+
+func pingSite(host: String = "http://[fd00::]:9172/hello", completion: @escaping (Bool) -> Void) {
+ guard let url = URL(string: host) else {
+ completion(false)
+ return
+ }
+
+ let config = URLSessionConfiguration.default
+ config.timeoutIntervalForRequest = 2.0
+ config.timeoutIntervalForResource = 2.0
+
+ let session = URLSession(configuration: config)
+
+ var request = URLRequest(url: url)
+ request.httpMethod = "GET"
+
+ let task = session.dataTask(with: request) { _, response, error in
+ if let error = error {
+ // print("Ping failed: \(error.localizedDescription)")
+ completion(false)
+ } else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
+ completion(true)
+ } else {
+ let httpResponse = response as? HTTPURLResponse
+ completion(false)
+ }
+ }
+
+ task.resume()
+}
+
+
+func presentAlert(title: String, message: String, completion: (() -> Void)? = nil) {
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+ let lastWindow = windowScene.windows.last {
+ let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
+ alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
+ completion?()
+ })
+
+ DispatchQueue.main.async {
+ lastWindow.rootViewController?.present(alert, animated: true)
+ }
+ }
+}
+
+
struct LaunchApp: Codable {
- let ok: Bool
- let error: String?
- let launching: Bool
- let position: Int?
- let mounting: Bool
+ let success: Bool
+ let message: String
}
func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
@@ -46,28 +130,23 @@ func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
var message = ""
- if let error = result.error {
- message = "Error: \(error)"
- } else if result.mounting {
- message = "App is mounting..."
- } else if result.launching {
- message = "App is launching..."
+ if !result.success {
+ message += "\n\(result.message)"
+
+
+ let alert = UIAlertController(title: "JIT Error", message: message, preferredStyle: .alert)
+ alert.addAction(UIAlertAction(title: "OK", style: .default))
+
+ DispatchQueue.main.async {
+ viewController.present(alert, animated: true)
+ }
} else {
- message = "App launch status unknown."
- }
-
- if let position = result.position {
- message += "\nPosition: \(position)"
- }
-
- let alert = UIAlertController(title: "Launch Status", message: message, preferredStyle: .alert)
- alert.addAction(UIAlertAction(title: "OK", style: .default))
-
- DispatchQueue.main.async {
- viewController.present(alert, animated: true)
+ // print("Hopefully JIT is enabled now...")
+ Ryujinx.shared.ryuIsJITEnabled()
}
} catch {
+ // print(String(data: jsonData, encoding: .utf8))
let alert = UIAlertController(title: "Decoding Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
diff --git a/src/MeloNX/MeloNX/App/Core/JIT/StikJIT/StikEnableJIT.swift b/src/MeloNX/MeloNX/App/Core/JIT/StikJIT/StikEnableJIT.swift
new file mode 100644
index 000000000..2cde8343d
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Core/JIT/StikJIT/StikEnableJIT.swift
@@ -0,0 +1,74 @@
+//
+// EnableJIT.swift
+// MeloNX
+//
+// Created by Stossy11 on 10/02/2025.
+//
+
+import Foundation
+import Network
+import UIKit
+
+func stikJITorStikDebug() -> Int {
+ let teamid = SecTaskCopyTeamIdentifier(SecTaskCreateFromSelf(nil)!, nil)
+
+ if checkifappinstalled("com.stik.sj") {
+ return 1 // StikDebug
+ }
+
+ if checkifappinstalled("com.stik.sj.\(String(teamid ?? ""))") {
+ return 2 // StikJIT
+ }
+
+ return 0 // Not Found
+}
+
+func checkforOld() -> Bool {
+ let teamid = SecTaskCopyTeamIdentifier(SecTaskCreateFromSelf(nil)!, nil)
+
+ if checkifappinstalled(changeAppUI("Y29tLnN0b3NzeTExLlBvbWVsbw==") ?? "") {
+ return true
+ }
+
+ if checkifappinstalled(changeAppUI("Y29tLnN0b3NzeTExLlBvbWVsbw==") ?? "" + ".\(String(teamid ?? ""))") {
+ return true
+ }
+
+ if checkifappinstalled((Bundle.main.bundleIdentifier ?? "").replacingOccurrences(of: "MeloNX", with: changeAppUI("UG9tZWxv") ?? "")) {
+ return true
+ }
+
+ return false
+}
+
+
+func checkifappinstalled(_ id: String) -> Bool {
+ guard let handle = dlopen("/System/Library/PrivateFrameworks/SpringBoardServices.framework/SpringBoardServices", RTLD_LAZY) else {
+ return false
+ }
+
+ typealias SBSLaunchApplicationWithIdentifierFunc = @convention(c) (CFString, Bool) -> Int32
+ guard let sym = dlsym(handle, "SBSLaunchApplicationWithIdentifier") else {
+ if let error = dlerror() {
+ print(String(cString: error))
+ }
+ dlclose(handle)
+ return false
+ }
+
+ let bundleID: CFString = id as CFString
+ let suspended: Bool = false
+
+
+ let SBSLaunchApplicationWithIdentifier = unsafeBitCast(sym, to: SBSLaunchApplicationWithIdentifierFunc.self)
+ let result = SBSLaunchApplicationWithIdentifier(bundleID, suspended)
+
+ return result == 9
+}
+
+func enableJITStik() {
+ let urlScheme = "stikjit://enable-jit?bundle-id=\(Bundle.main.bundleIdentifier ?? "wow")"
+ if let launchURL = URL(string: urlScheme), !isJITEnabled() {
+ UIApplication.shared.open(launchURL, options: [:], completionHandler: nil)
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/BaseController.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/BaseController.swift
new file mode 100644
index 000000000..34bbb79a5
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/BaseController.swift
@@ -0,0 +1,14 @@
+//
+// BaseController.swift
+// MeloNX
+//
+// Created by MediaMoots on 5/17/2025.
+//
+
+//──────────────────────────────────────────────────────────────────────── MARK:- Base Controller Protocol
+
+/// Base Controller with motion related functions
+protocol BaseController: AnyObject {
+ func tryRegisterMotion(slot: UInt8)
+ func tryGetMotionProvider() -> DSUMotionProvider?
+}
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Motion/DSUMotionProviders.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Motion/DSUMotionProviders.swift
new file mode 100644
index 000000000..601115809
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Motion/DSUMotionProviders.swift
@@ -0,0 +1,178 @@
+//
+// DSUMotionProviders.swift
+//
+// Multi-source Cemuhook-compatible DSU server.
+// Created by MediaMoots on 5/17/2025.
+//
+//
+
+import CoreMotion
+import GameController // GCController
+
+//──────────────────────────────────────────────────────────────────────── MARK:- Providers
+
+/// iPhone / iPad IMU
+final class DeviceMotionProvider: DSUMotionProvider {
+
+ // ───── DSUMotionProvider conformance
+ let slot: UInt8
+ let mac: [UInt8] = [0xAB,0x12,0xCD,0x34,0xEF,0x56]
+ let connectionType: UInt8 = 2
+ let batteryLevel: UInt8 = 5
+ let motionRate: Double = 60.0 // 60 Hz
+
+ // ───── Internals
+ private let mm = CMMotionManager()
+
+ // Thread Safety
+ private let dataLock = NSLock()
+ private var _latest: CMDeviceMotion?
+ private var latest: CMDeviceMotion? {
+ get { dataLock.lock(); defer { dataLock.unlock() }; return _latest }
+ set { dataLock.lock(); _latest = newValue; dataLock.unlock() }
+ }
+
+ private var orientation: UIDeviceOrientation =
+ UIDevice.current.orientation == .unknown ? .landscapeLeft : UIDevice.current.orientation
+
+ init(slot: UInt8) {
+ precondition(slot < 8, "DSU only supports slots 0…7")
+ self.slot = slot
+
+ // ── start Core Motion
+ mm.deviceMotionUpdateInterval = 1.0 / motionRate
+ mm.startDeviceMotionUpdates(to: .main) { [weak self] m, _ in
+ guard let self = self, let m = m else { return }
+ self.latest = m
+ if let sample = self.nextSample() {
+ DSUServer.shared.pushSample(sample, from: self)
+ }
+ }
+
+ // ── track orientation changes (ignore flat)
+ UIDevice.current.beginGeneratingDeviceOrientationNotifications()
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(orientationDidChange),
+ name: UIDevice.orientationDidChangeNotification,
+ object: nil
+ )
+ }
+
+ @objc private func orientationDidChange() {
+ let o = UIDevice.current.orientation
+ if o.isFlat { return } // ignore face-up / face-down
+ orientation = o
+ }
+
+ func nextSample() -> DSUMotionSample? {
+ guard let m = latest else { return nil }
+
+ // Raw values
+ let gx = Float(m.rotationRate.x)
+ let gy = Float(m.rotationRate.y)
+ let gz = Float(m.rotationRate.z)
+ let ax = Float(m.gravity.x + m.userAcceleration.x)
+ let ay = Float(m.gravity.y + m.userAcceleration.y)
+ let az = Float(m.gravity.z + m.userAcceleration.z)
+
+ // Rotate axes to match Cemuhook's "landscape-left as neutral" convention
+ let a: SIMD3
+ let g: SIMD3
+
+ switch orientation {
+ case .portrait:
+ a = SIMD3( ax, az, -ay)
+ g = SIMD3( gx, -gz, gy)
+ case .landscapeRight:
+ a = SIMD3( ay, az, ax)
+ g = SIMD3( gy, -gz, -gx)
+ case .portraitUpsideDown:
+ a = SIMD3( -ax, az, ay)
+ g = SIMD3( -gx, -gz, -gy)
+ case .landscapeLeft, .unknown, .faceUp, .faceDown:
+ a = SIMD3( -ay, az, -ax)
+ g = SIMD3( -gy, -gz, gx)
+ @unknown default:
+ return nil
+ }
+
+ // Convert gyro rad/s → °/s here so the server doesn't have to.
+ let gDeg = g * (180 / .pi)
+
+ return DSUMotionSample(timestampUS: currentUS(),
+ accel: a,
+ gyroDeg: gDeg)
+ }
+}
+
+// Any Switch Pro / DualSense controller that exposes `GCMotion`
+final class ControllerMotionProvider: DSUMotionProvider {
+
+ // DSUMotionProvider
+ let slot: UInt8
+ let mac: [UInt8] = [0xAB,0x12,0xCD,0x34,0xEF,0x56]
+ let connectionType: UInt8 = 2
+ var batteryLevel: UInt8 {
+ UInt8((pad.battery?.batteryLevel ?? 0.3) * 5).clamped(to: 0...5)
+ }
+
+ private let pad: GCController
+
+ // Thread Safety
+ private let dataLock = NSLock()
+ private var _latest: GCMotion?
+ private var latest: GCMotion? {
+ get { dataLock.lock(); defer { dataLock.unlock() }; return _latest }
+ set { dataLock.lock(); _latest = newValue; dataLock.unlock() }
+ }
+
+ init(controller: GCController, slot: UInt8) {
+ self.pad = controller
+ self.slot = slot
+ pad.motion?.sensorsActive = true
+ pad.motion?.valueChangedHandler = { [weak self] motion in
+ guard let self = self else { return }
+ self.latest = motion
+ if let sample = self.nextSample() {
+ DSUServer.shared.pushSample(sample, from: self)
+ }
+ }
+ }
+
+ func nextSample() -> DSUMotionSample? {
+ guard let m = latest else { return nil }
+
+ // Extract and convert acceleration to SIMD3
+ let a = SIMD3(
+ Float(m.acceleration.x),
+ Float(m.acceleration.z),
+ -Float(m.acceleration.y)
+ )
+
+ // Extract, transform, and convert rotation rate to SIMD3 (in radians/s)
+ let g = SIMD3(
+ Float(m.rotationRate.x),
+ -Float(m.rotationRate.z),
+ Float(m.rotationRate.y)
+ )
+
+ // Convert gyro rotation rate from rad/s to degrees/s
+ let gDeg = g * (180 / .pi)
+
+ return DSUMotionSample(
+ timestampUS: currentUS(),
+ accel: a,
+ gyroDeg: gDeg
+ )
+ }
+}
+
+//──────────────────────────────────────────────────────────────────────── MARK:- Helper funcs / ext
+
+private func uint64US(_ time: TimeInterval) -> UInt64 { UInt64(time * 1_000_000) }
+private func currentUS() -> UInt64 { uint64US(CACurrentMediaTime()) }
+
+private extension Comparable {
+ func clamped(to r: ClosedRange) -> Self { min(max(self, r.lowerBound), r.upperBound) }
+}
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Motion/DSUServer.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Motion/DSUServer.swift
new file mode 100644
index 000000000..9947fae5e
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Motion/DSUServer.swift
@@ -0,0 +1,217 @@
+//
+// DSUServer.swift
+//
+// Multi-source Cemuhook-compatible DSU server.
+// Created by MediaMoots on 5/17/2025.
+//
+//
+
+import Foundation
+import CocoaAsyncSocket // GCDAsyncUdpSocket
+import zlib // CRC-32
+
+//──────────────────────────────────────────────────────────────────────── MARK:- DSU Motion protocol
+
+/// One motion source == one DSU *slot* (0-7).
+protocol DSUMotionProvider: AnyObject {
+ var slot: UInt8 { get } // unique, 0-7
+ var mac: [UInt8] { get } // 6-byte ID
+ var connectionType: UInt8 { get } // 0 = USB, 2 = BT
+ var batteryLevel: UInt8 { get } // 0-5 (Cemuhook)
+
+ func nextSample() -> DSUMotionSample?
+}
+
+/// Raw motion payload returned by providers.
+struct DSUMotionSample {
+ var timestampUS: UInt64 // µs
+ var accel: SIMD3 // G's
+ var gyroDeg: SIMD3 // °/s
+}
+
+//──────────────────────────────────────────────────────────────────────── MARK:- Server constants
+
+private enum C {
+ static let port: UInt16 = 26_760
+ static let protocolVersion: UInt16 = 1_001
+ static let headerMagic = "DSUS"
+}
+
+//──────────────────────────────────────────────────────────────────────── MARK:- Server core
+
+final class DSUServer: NSObject {
+
+ // Singleton for convenience
+ static let shared = DSUServer()
+ private override init() {
+ serverID = UInt32.random(in: .min ... .max)
+ super.init()
+ configureSocket()
+ }
+
+ // MARK: Public API ─────────────────────────────────────────────
+ func register(_ provider: DSUMotionProvider) { providers[provider.slot] = provider }
+ func unregister(slot: UInt8) { providers.removeValue(forKey: slot) }
+
+ /// 🔸 providers push fresh samples here.
+ func pushSample(_ sample: DSUMotionSample, from provider: DSUMotionProvider) {
+ guard let addr = lastClientAddress else { return } // no subscriber → drop
+ sendPadData(sample: sample, from: provider, to: addr)
+ }
+
+ // MARK: Private
+ private let serverID: UInt32
+ private var socket: GCDAsyncUdpSocket?
+ private var lastClientAddress: Data?
+
+ private var providers = [UInt8 : DSUMotionProvider]() // slot→provider
+ private var packetNumber = [UInt8 : UInt32]() // per-slot counter
+
+ // ───────── UDP setup
+ private func configureSocket() {
+ socket = GCDAsyncUdpSocket(delegate: self, delegateQueue: .main)
+ do {
+ try socket?.bind(toPort: C.port)
+ try socket?.beginReceiving()
+ //print("🟢 DSU server listening on UDP \(C.port)")
+ } catch {
+ //print("❌ DSU socket error:", error)
+ }
+ }
+}
+
+//──────────────────────────────────────────────────────────────────────── MARK:- UDP delegate
+
+extension DSUServer: GCDAsyncUdpSocketDelegate {
+
+ func udpSocket(_ sock: GCDAsyncUdpSocket,
+ didReceive data: Data,
+ fromAddress addr: Data,
+ withFilterContext ctx: Any?) {
+
+ lastClientAddress = addr
+
+ // Light validation
+ guard data.count >= 20,
+ String(decoding: data[0..<4], as: UTF8.self) == C.headerMagic,
+ data.readUInt16LE(at: 4) == C.protocolVersion
+ else { return }
+
+ let type = data.readUInt32LE(at: 16)
+ switch type {
+ case 0x100001: sendPortInfo(to: addr) // client asks for port list
+ case 0x100002: break // subscription acknowledged
+ default: break
+ }
+ }
+
+ func udpSocketDidClose(_ sock: GCDAsyncUdpSocket, withError err: Error?) {
+ //print("UDP closed:", err?.localizedDescription ?? "nil")
+ lastClientAddress = nil
+ }
+}
+
+//──────────────────────────────────────────────────────────────────────── MARK:- Packet helpers
+
+private extension DSUServer {
+
+ // ── Header (16 bytes)
+ func appendHeader(into d: inout Data, payloadSize: UInt16) {
+ d.append(C.headerMagic.data(using: .utf8)!) // "DSUS"
+ d.append(C.protocolVersion.leData) // Protocol Version
+ d.append(payloadSize.leData) // Payload Size
+ d.append(Data(repeating: 0, count: 4)) // CRC-stub
+ d.append(serverID.leData) // Server ID
+ }
+ func patchCRC32(of packet: inout Data) {
+ let crc = packet.withUnsafeBytes { ptr in
+ crc32(0, ptr.baseAddress, uInt(packet.count))
+ }.littleEndian
+ let crcLE = UInt32(crc).littleEndian
+ let crcData = withUnsafeBytes(of: crcLE) { Data($0) }
+ packet.replaceSubrange(8..<12, with: crcData)
+ }
+
+ // ── 0x100001 DSUSPortInfo
+ func sendPortInfo(to addr: Data) {
+ for p in providers.values {
+ var pkt = Data()
+ appendHeader(into: &pkt, payloadSize: 12)
+ pkt.append(UInt32(0x100001).leData)
+
+ pkt.append(p.slot)
+ pkt.append(UInt8(2)) // connected
+ pkt.append(UInt8(2)) // full gyro
+ pkt.append(p.connectionType)
+ pkt.append(p.mac, count: 6)
+ pkt.append(p.batteryLevel)
+ pkt.append(UInt8(0)) // padding
+
+ patchCRC32(of: &pkt)
+ socket?.send(pkt, toAddress: addr, withTimeout: -1, tag: 0)
+ }
+ }
+
+ // ── 0x100002 DSUSPadDataRsp
+ func sendPadData(sample s: DSUMotionSample,
+ from p: DSUMotionProvider,
+ to addr: Data) {
+
+ var pkt = Data()
+ appendHeader(into: &pkt, payloadSize: 84)
+ pkt.append(UInt32(0x100002).leData)
+
+ pkt.append(p.slot)
+ pkt.append(UInt8(2)) // connected
+ pkt.append(UInt8(2)) // full gyro
+ pkt.append(p.connectionType)
+ pkt.append(p.mac, count: 6)
+ pkt.append(p.batteryLevel)
+ pkt.append(UInt8(1)) // is connected
+
+ let num = packetNumber[p.slot, default: 0]
+ pkt.append(num.leData)
+ packetNumber[p.slot] = num &+ 1
+
+ pkt.append(UInt16(0).leData) // buttons
+ pkt.append(contentsOf: [0,0]) // HOME / Touch
+ pkt.append(contentsOf: [128,128,128,128]) // sticks
+ pkt.append(Data(repeating: 0, count: 12)) // d-pad / face / trig
+ pkt.append(Data(repeating: 0, count: 12)) // touch 1 & 2
+ pkt.append(s.timestampUS.leData)
+
+ pkt.append(s.accel.x.leData)
+ pkt.append(s.accel.y.leData)
+ pkt.append(s.accel.z.leData)
+
+ pkt.append(s.gyroDeg.x.leData)
+ pkt.append(s.gyroDeg.y.leData)
+ pkt.append(s.gyroDeg.z.leData)
+
+ patchCRC32(of: &pkt)
+ socket?.send(pkt, toAddress: addr, withTimeout: -1, tag: 0)
+ }
+}
+
+//──────────────────────────────────────────────────────────────────────── MARK:- Helper funcs / ext
+
+private extension FixedWidthInteger {
+ var leData: Data {
+ var v = self.littleEndian
+ return Data(bytes: &v, count: MemoryLayout.size)
+ }
+}
+private extension Float {
+ var leData: Data {
+ var v = self
+ return Data(bytes: &v, count: MemoryLayout.size)
+ }
+}
+private extension Data {
+ func readUInt16LE(at offset: Int) -> UInt16 {
+ self[offset.. UInt32 {
+ self[offset.. DSUMotionProvider? { return controllerMotionProvider }
private func setupHandheldController() {
if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 {
@@ -49,52 +82,51 @@ class NativeController: Hashable {
// Update joystick state here
},
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.fromOpaque(userdata).takeUnretainedValue()
+ _self.nativeController.playerIndex = player
},
Rumble: { userdata, lowFreq, highFreq in
- print("Rumble with \(lowFreq), \(highFreq)")
guard let userdata else { return 0 }
let _self = Unmanaged.fromOpaque(userdata).takeUnretainedValue()
- VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq), engine: _self.controllerHaptics)
+ _self.rumbleController?.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
return 0
},
RumbleTriggers: { userdata, leftRumble, rightRumble in
- print("Trigger rumble with \(leftRumble), \(rightRumble)")
return 0
},
SetLED: { userdata, red, green, blue in
- print("Set LED to RGB(\(red), \(green), \(blue))")
+ guard let userdata else { return 0 }
+ let _self = Unmanaged.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
},
SendEffect: { userdata, data, size in
- print("Effect sent with size \(size)")
return 0
}
)
- instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1)
+ instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)
if instanceID < 0 {
- print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
return
}
- // Open a game controller for the virtual joystick
- let joystick = SDL_JoystickFromInstanceID(instanceID)
controller = SDL_GameControllerOpen(Int32(instanceID))
if controller == nil {
- print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
return
}
if #available(iOS 16, *) {
guard let gamepad = nativeController.extendedGamepad
else { return }
-
- setupButtonChangeListener(gamepad.buttonA, for: .B)
- setupButtonChangeListener(gamepad.buttonB, for: .A)
- setupButtonChangeListener(gamepad.buttonX, for: .Y)
- setupButtonChangeListener(gamepad.buttonY, for: .X)
+
+ setupButtonChangeListener(gamepad.buttonA, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .B : .A)
+ setupButtonChangeListener(gamepad.buttonB, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .A : .B)
+ setupButtonChangeListener(gamepad.buttonX, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .Y : .X)
+ setupButtonChangeListener(gamepad.buttonY, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .X : .Y)
setupButtonChangeListener(gamepad.dpad.up, for: .dPadUp)
setupButtonChangeListener(gamepad.dpad.down, for: .dPadDown)
@@ -141,49 +173,13 @@ class NativeController: Hashable {
func setupTriggerChangeListener(_ button: GCControllerButtonInput, for key: ThumbstickType) {
button.valueChangedHandler = { [unowned self] _, value, pressed in
-// print("Value: \(value), Is pressed: \(pressed)")
+// // print("Value: \(value), Is pressed: \(pressed)")
let axis: SDL_GameControllerAxis = (key == .left) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
let scaledValue = Sint16(value * 32767.0)
updateAxisValue(value: scaledValue, forAxis: axis)
}
}
- static func rumble(lowFreq: Float, highFreq: Float) {
- do {
- // Low-frequency haptic pattern
- let lowFreqPattern = try CHHapticPattern(events: [
- CHHapticEvent(eventType: .hapticTransient, parameters: [
- CHHapticEventParameter(parameterID: .hapticIntensity, value: lowFreq),
- CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
- ], relativeTime: 0, duration: 0.2)
- ], parameters: [])
-
- // High-frequency haptic pattern
- let highFreqPattern = try CHHapticPattern(events: [
- CHHapticEvent(eventType: .hapticTransient, parameters: [
- CHHapticEventParameter(parameterID: .hapticIntensity, value: highFreq),
- CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
- ], relativeTime: 0.2, duration: 0.2)
- ], parameters: [])
-
- // Create and start the haptic engine
- let engine = try CHHapticEngine()
- try engine.start()
-
- // Create and play the low-frequency player
- let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
- try lowFreqPlayer.start(atTime: 0)
-
- // Create and play the high-frequency player after a short delay
- let highFreqPlayer = try engine.makePlayer(with: highFreqPattern)
- try highFreqPlayer.start(atTime: 0.2)
-
- } catch {
- print("Error creating haptic patterns: \(error)")
- }
- }
-
-
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
guard controller != nil else { return }
let joystick = SDL_JoystickFromInstanceID(instanceID)
@@ -208,7 +204,6 @@ class NativeController: Hashable {
func setButtonState(_ state: Uint8, for button: VirtualControllerButton) {
guard controller != nil else { return }
-// print("Button: \(button.rawValue) {state: \(state)}")
if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) {
let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
let value: Int = (state == 1) ? 32767 : 0
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Rumble/RumbleController.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Rumble/RumbleController.swift
new file mode 100644
index 000000000..2f7793af7
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Rumble/RumbleController.swift
@@ -0,0 +1,132 @@
+//
+// RumbleController.swift
+// MeloNX
+//
+// Created by MediaMoots on 2025/5/24.
+//
+
+import CoreHaptics
+import Foundation
+
+class RumbleController {
+
+ private var engine: CHHapticEngine?
+ private var lowHapticPlayer: CHHapticPatternPlayer?
+ private var highHapticPlayer: CHHapticPatternPlayer?
+ private var rumbleMultiplier: Float = 1.0
+
+ // The duration of each continuous haptic event.
+ // We'll restart the players before this duration expires.
+ private let hapticEventDuration: TimeInterval = 20
+
+ // Timer to schedule player restarts
+ private var playerRestartTimer: Timer?
+
+ // Interval before the haptic event duration runs out to restart
+ private let restartGracePeriod: TimeInterval = 1.0
+
+ init (engine: CHHapticEngine?, rumbleMultiplier: Float) {
+ self.engine = engine
+ self.rumbleMultiplier = rumbleMultiplier
+
+ createPlayers()
+ setupPlayerRestartTimer()
+ }
+
+ // Deinitializer to clean up the timer and stop players when the controller is deallocated
+ deinit {
+ playerRestartTimer?.invalidate() // Stop the timer
+ playerRestartTimer = nil
+
+ // Optionally stop the haptic players immediately
+ try? lowHapticPlayer?.stop(atTime: CHHapticTimeImmediate)
+ try? highHapticPlayer?.stop(atTime: CHHapticTimeImmediate)
+
+ // print("RumbleController deinitialized.")
+ }
+
+ // MARK: - Private Methods for Player Management
+ private func createPlayers() {
+ // Ensure the engine is available before proceeding
+ guard let engine = self.engine else {
+ // print("CHHapticEngine is nil. Cannot initialize RumbleController.")
+ return
+ }
+
+ do {
+ let baseIntensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0)
+
+ let lowSharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.0)
+ let highSharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 1)
+
+ // Create continuous haptic events with the defined duration
+ let lowContinuousEvent = CHHapticEvent(eventType: .hapticContinuous, parameters: [baseIntensity, lowSharpness], relativeTime: 0, duration: hapticEventDuration)
+ let highContinuousEvent = CHHapticEvent(eventType: .hapticContinuous, parameters: [baseIntensity, highSharpness], relativeTime: 0, duration: hapticEventDuration)
+
+ // Create patterns from the continuous haptic events.
+ let lowPattern = try CHHapticPattern(events: [lowContinuousEvent], parameters: [])
+ let highPattern = try CHHapticPattern(events: [highContinuousEvent], parameters: [])
+
+ // Make players from the patterns
+ lowHapticPlayer = try engine.makePlayer(with: lowPattern)
+ highHapticPlayer = try engine.makePlayer(with: highPattern)
+
+ rumble(lowFreq: 0, highFreq: 0)
+
+ // Start players initially
+ try lowHapticPlayer?.start(atTime: 0)
+ try highHapticPlayer?.start(atTime: 0)
+ } catch {
+ // print("Error initializing RumbleController or setting up haptic player: \(error.localizedDescription)")
+
+ // Clean up if setup fails
+ lowHapticPlayer = nil
+ highHapticPlayer = nil
+ playerRestartTimer?.invalidate()
+ playerRestartTimer = nil
+ }
+ }
+
+ private func setupPlayerRestartTimer() {
+ // Invalidate any existing timer to prevent multiple timers if init is called multiple times
+ playerRestartTimer?.invalidate()
+
+ // Calculate the interval for restarting: 1 second before the haptic event duration ends
+ let restartInterval = hapticEventDuration - restartGracePeriod
+
+ guard restartInterval > 0 else {
+ // print("Warning: hapticEventDuration (\(hapticEventDuration)s) is too short for scheduled restart with grace period (\(restartGracePeriod)s). Timer will not be set.")
+ return
+ }
+
+ // Schedule a repeating timer that calls restartPlayers()
+ playerRestartTimer = Timer.scheduledTimer(withTimeInterval: restartInterval, repeats: true) { [weak self] _ in
+ self?.createPlayers()
+ }
+ // Ensure the timer is added to the current run loop in its default mode
+ RunLoop.current.add(playerRestartTimer!, forMode: .default)
+
+ // print("Haptic Players restart timer scheduled to fire every \(restartInterval) seconds.")
+ }
+
+ // MARK: - Public Rumble Control
+
+ public func rumble(lowFreq: Float, highFreq: Float) {
+
+ // Normalize SDL values (0-65535) to CoreHaptics range (0.0-1.0)
+ let normalizedLow = min(1.0, max(0.0, lowFreq * rumbleMultiplier / 65535.0))
+ let normalizedHigh = min(1.0, max(0.0, highFreq * rumbleMultiplier / 65535.0))
+
+ // Create dynamic parameters to control intensity
+ let lowIntensityParameter = CHHapticDynamicParameter(parameterID: .hapticIntensityControl, value: normalizedLow, relativeTime: 0)
+ let highIntensityParameter = CHHapticDynamicParameter(parameterID: .hapticIntensityControl, value: normalizedHigh, relativeTime: 0)
+
+ // Send parameters to the players
+ do {
+ try lowHapticPlayer?.sendParameters([lowIntensityParameter], atTime: 0)
+ try highHapticPlayer?.sendParameters([highIntensityParameter], atTime: 0)
+ } catch {
+ // print("Error sending haptic parameters: \(error.localizedDescription)")
+ }
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift
index 7fa5c33c1..9ffbcf85d 100644
--- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift
@@ -9,16 +9,49 @@ import Foundation
import CoreHaptics
import UIKit
-class VirtualController {
+class VirtualController : BaseController {
private var instanceID: SDL_JoystickID = -1
private var controller: OpaquePointer?
+ private let hapticEngine: CHHapticEngine?
+ private let rumbleController: RumbleController?
+ private var deviceMotionProvider: DeviceMotionProvider?
public let controllername = "MeloNX Touch Controller"
init() {
+ // Setup Haptics
+ hapticEngine = try? CHHapticEngine()
+ if let hapticsEngine = hapticEngine {
+ do {
+ try hapticsEngine.start()
+ rumbleController = RumbleController(engine: hapticsEngine, rumbleMultiplier: 2.0)
+
+ // print("CHHapticEngine started and RumbleController initialized.")
+ } catch {
+ // print("Error starting CHHapticEngine: \(error.localizedDescription)")
+ rumbleController = nil
+ }
+ } else {
+ // print("CHHapticEngine is nil. Cannot initialize RumbleController.")
+ rumbleController = nil
+ }
setupVirtualController()
}
+ internal func tryRegisterMotion(slot: UInt8) {
+ // Setup Motion
+ let dsuServer = DSUServer.shared
+
+ deviceMotionProvider = DeviceMotionProvider(slot: slot)
+ if let provider = deviceMotionProvider {
+ dsuServer.register(provider)
+ }
+ }
+
+ internal func tryGetMotionProvider() -> DSUMotionProvider? {
+ return deviceMotionProvider
+ }
+
private func setupVirtualController() {
if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 {
SDL_InitSubSystem(Uint32(SDL_INIT_GAMECONTROLLER))
@@ -36,100 +69,50 @@ class VirtualController {
button_mask: 0,
axis_mask: 0,
name: controllername.withCString { $0 },
- userdata: nil,
+ userdata: Unmanaged.passUnretained(self).toOpaque(),
Update: { userdata in
// Update joystick state here
},
SetPlayerIndex: { userdata, playerIndex in
- print("Player index set to \(playerIndex)")
+ // print("Player index set to \(playerIndex)")
},
Rumble: { userdata, lowFreq, highFreq in
- print("Rumble with \(lowFreq), \(highFreq)")
- VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
+ // print("Rumble with \(lowFreq), \(highFreq)")
+ if UIDevice.current.userInterfaceIdiom == .phone {
+ guard let userdata else { return 0 }
+ let _self = Unmanaged.fromOpaque(userdata).takeUnretainedValue()
+ _self.rumbleController?.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
+ }
return 0
},
RumbleTriggers: { userdata, leftRumble, rightRumble in
- print("Trigger rumble with \(leftRumble), \(rightRumble)")
+ // print("Trigger rumble with \(leftRumble), \(rightRumble)")
return 0
},
SetLED: { userdata, red, green, blue in
- print("Set LED to RGB(\(red), \(green), \(blue))")
+ // print("Set LED to RGB(\(red), \(green), \(blue))")
return 0
},
SendEffect: { userdata, data, size in
- print("Effect sent with size \(size)")
+ // print("Effect sent with size \(size)")
return 0
}
)
instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1)
if instanceID < 0 {
- print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
+ // print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
return
}
- // Open a game controller for the virtual joystick
- let joystick = SDL_JoystickFromInstanceID(instanceID)
controller = SDL_GameControllerOpen(Int32(instanceID))
if controller == nil {
- print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
+ // print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
return
}
}
- static func rumble(lowFreq: Float, highFreq: Float, engine: CHHapticEngine? = nil) {
- do {
- // Low-frequency haptic pattern
- let lowFreqPattern = try CHHapticPattern(events: [
- CHHapticEvent(eventType: .hapticTransient, parameters: [
- CHHapticEventParameter(parameterID: .hapticIntensity, value: lowFreq),
- CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
- ], relativeTime: 0, duration: 0.2)
- ], parameters: [])
-
- // High-frequency haptic pattern
- let highFreqPattern = try CHHapticPattern(events: [
- CHHapticEvent(eventType: .hapticTransient, parameters: [
- CHHapticEventParameter(parameterID: .hapticIntensity, value: highFreq),
- CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
- ], relativeTime: 0.2, duration: 0.2)
- ], parameters: [])
-
- // Mutable engine
- var engine = engine
-
- // If no engine passed, use device engine
- if engine == nil {
- // Create and start the haptic engine
- if hapticEngine == nil {
- hapticEngine = try CHHapticEngine()
- try hapticEngine?.start()
- }
-
- engine = hapticEngine
- }
-
- guard let engine else {
- return print("Error creating haptic patterns: hapticEngine is nil")
- }
-
- // Create and play the low-frequency player
- let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
- try lowFreqPlayer.start(atTime: 0)
-
- // Create and play the high-frequency player after a short delay
- let highFreqPlayer = try engine.makePlayer(with: highFreqPattern)
- try highFreqPlayer.start(atTime: 0.2)
-
- } catch {
- print("Error creating haptic patterns: \(error)")
- }
- }
-
- private static var hapticEngine: CHHapticEngine?
-
-
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
guard controller != nil else { return }
let joystick = SDL_JoystickFromInstanceID(instanceID)
@@ -137,10 +120,8 @@ class VirtualController {
}
func thumbstickMoved(_ stick: ThumbstickType, x: Double, y: Double) {
- let scaleFactor = 32767.0 / 160
-
- let scaledX = Int16(min(32767.0, max(-32768.0, x * scaleFactor)))
- let scaledY = Int16(min(32767.0, max(-32768.0, y * scaleFactor)))
+ let scaledX = Int16(min(32767.0, max(-32768.0, x * 32767.0)))
+ let scaledY = Int16(min(32767.0, max(-32768.0, y * 32767.0)))
if stick == .right {
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue))
@@ -154,7 +135,7 @@ class VirtualController {
func setButtonState(_ state: Uint8, for button: VirtualControllerButton) {
guard controller != nil else { return }
- print("Button: \(button.rawValue) {state: \(state)}")
+ // // print("Button: \(button.rawValue) {state: \(state)}")
if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) {
let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
let value: Int = (state == 1) ? 32767 : 0
@@ -178,10 +159,10 @@ class VirtualController {
}
enum VirtualControllerButton: Int {
- case B
case A
- case Y
+ case B
case X
+ case Y
case back
case guide
case start
@@ -195,6 +176,24 @@ enum VirtualControllerButton: Int {
case dPadRight
case leftTrigger
case rightTrigger
+
+ var isTrigger: Bool {
+ switch self {
+ case .leftTrigger, .rightTrigger, .leftShoulder, .rightShoulder:
+ return true
+ default:
+ return false
+ }
+ }
+
+ var isSmall: Bool {
+ switch self {
+ case .back, .start, .guide:
+ return true
+ default:
+ return false
+ }
+ }
}
enum ThumbstickType: Int {
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/DisplayVisible.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/DisplayVisible.swift
deleted file mode 100644
index fd5d2c302..000000000
--- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/DisplayVisible.swift
+++ /dev/null
@@ -1,104 +0,0 @@
-//
-// Untitled.swift
-// MeloNX
-//
-// Created by Stossy11 on 28/11/2024.
-//
-
-import Foundation
-import GameController
-import UIKit
-import SwiftUI
-
-
-
-var theWindow: UIWindow? = nil
-extension UIWindow {
- // Makes the SDLWindow use the current WindowScene instead of making its own window.
- // Also waits for the window to append the on-screen controller
- @objc func wdb_makeKeyAndVisible() {
- let enabled = UserDefaults.standard.bool(forKey: "oldWindowCode")
-
- if #unavailable(iOS 17.0), enabled {
- self.windowScene = (UIApplication.shared.connectedScenes.first! as! UIWindowScene)
- }
-
- self.wdb_makeKeyAndVisible()
- theWindow = self
-
- if #available(iOS 17, *) {
- Ryujinx.shared.repeatuntilfindLayer()
- } else if UserDefaults.standard.bool(forKey: "isVirtualController") && enabled {
- waitForController()
- }
- }
-}
-
-// MARK: - iOS 16 and below Only
-
-var hostingController: UIHostingController?
-func waitForController() {
- guard let window = theWindow else { return }
-
- // Function to search for an existing UIHostingController with ControllerView
- func findGCControllerView(in view: UIView) -> UIHostingController? {
- if let hostingVC = view.next as? UIHostingController {
- return hostingVC
- }
-
- for subview in view.subviews {
- if let found = findGCControllerView(in: subview) {
- return found
- }
- }
-
- return nil
- }
-
- let controllerView = ControllerView()
- let newHostingController = UIHostingController(rootView: controllerView)
-
- hostingController = newHostingController
-
- let containerView = newHostingController.view!
- containerView.backgroundColor = .clear
- containerView.frame = window.bounds
- containerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
-
- // Timer for controller
- Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
- if findGCControllerView(in: window) == nil {
- // Adds Virtual Controller Subview
- window.addSubview(containerView)
- window.bringSubviewToFront(containerView)
-
- if let sdlWindow = SDL_GetWindowFromID(1) {
- SDL_SetWindowPosition(sdlWindow, 0, 0)
- }
-
- timer.invalidate()
- }
- }
-}
-
-
-class TransparentHostingContainerView: UIView {
- override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
- // Check if the point is within the subviews of this container
- let view = super.hitTest(point, with: event)
- print(view)
-
- // Return nil if the touch is outside visible content (passes through to views below)
- return view === self ? nil : view
- }
-}
-
-// Patches makeKeyAndVisible to wdb_makeKeyAndVisible
-func patchMakeKeyAndVisible() {
- let uiwindowClass = UIWindow.self
- if let m1 = class_getInstanceMethod(uiwindowClass, #selector(UIWindow.makeKeyAndVisible)),
- let m2 = class_getInstanceMethod(uiwindowClass, #selector(UIWindow.wdb_makeKeyAndVisible)) {
- method_exchangeImplementations(m1, m2)
- }
-}
-
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/MemoryDisplay/MemoryUsageMonitor.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/MemoryDisplay/MemoryUsageMonitor.swift
index 06070a38b..22cdebc25 100644
--- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/MemoryDisplay/MemoryUsageMonitor.swift
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/MemoryDisplay/MemoryUsageMonitor.swift
@@ -13,7 +13,7 @@ class MemoryUsageMonitor: ObservableObject {
private var timer: Timer?
init() {
- timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
+ timer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { [weak self] _ in
self?.updateMemoryUsage()
}
}
@@ -32,11 +32,12 @@ class MemoryUsageMonitor: ObservableObject {
}
if result == KERN_SUCCESS {
+ memoryUsage = 0
memoryUsage = taskInfo.phys_footprint
}
else {
- print("Error with task_info(): " +
- (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
+ // print("Error with task_info(): " +
+ // (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
}
}
@@ -46,7 +47,6 @@ class MemoryUsageMonitor: ObservableObject {
formatter.countStyle = .memory
return formatter.string(fromByteCount: Int64(bytes))
}
-
}
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/PerformanceDisplay/PerformanceOverlay.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/PerformanceDisplay/PerformanceOverlay.swift
deleted file mode 100644
index ac014ff4a..000000000
--- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/PerformanceDisplay/PerformanceOverlay.swift
+++ /dev/null
@@ -1,22 +0,0 @@
-//
-// Untitled.swift
-// MeloNX
-//
-// Created by Stossy11 on 21/12/2024.
-//
-
-import SwiftUI
-
-struct PerformanceOverlayView: View {
- @StateObject private var memorymonitor = MemoryUsageMonitor()
-
- @StateObject private var fpsmonitor = FPSMonitor()
-
- var body: some View {
- VStack {
- Text("\(fpsmonitor.formatFPS())")
- Text(memorymonitor.formatMemorySize(memorymonitor.memoryUsage))
- }
- }
-}
-
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/MetalHUD/MTLHUD.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/MetalHUD/MTLHUD.swift
index e89e9f6f4..8985973a6 100644
--- a/src/MeloNX/MeloNX/App/Core/Ryujinx/MetalHUD/MTLHUD.swift
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/MetalHUD/MTLHUD.swift
@@ -6,26 +6,28 @@
//
import Foundation
+import SwiftUI
-
-class MTLHud {
+class MTLHud: ObservableObject {
+ @Published var canMetalHud: Bool = false
- var canMetalHud: Bool {
- return openMetalDylib()
- }
-
- var isEnabled: Bool {
- if let getenv = getenv("MTL_HUD_ENABLED") {
- return String(cString: getenv).contains("1")
+ @AppStorage("MTL_HUD_ENABLED") var metalHudEnabled: Bool = false {
+ didSet {
+ if metalHudEnabled {
+ enable()
+ } else {
+ disable()
+ }
}
- return false
}
+
static let shared = MTLHud()
private init() {
- openMetalDylib()
- if UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED") {
+ canMetalHud = openMetalDylib() // i'm fixing the warnings just because you said i suck at coding Autumn (propenchiefer, https://youtu.be/tc65SNOTMz4 7:23)
+
+ if metalHudEnabled {
enable()
} else {
disable()
@@ -35,16 +37,9 @@ class MTLHud {
func openMetalDylib() -> Bool {
let path = "/usr/lib/libMTLHud.dylib"
- // Load the dynamic library
if dlopen(path, RTLD_NOW) != nil {
- // Library loaded successfully
- print("Library loaded from \(path)")
return true
} else {
- // Handle error
- if let error = String(validatingUTF8: dlerror()) {
- print("Error loading library: \(error)")
- }
return false
}
}
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift
index bd09f1b74..3c09d1905 100644
--- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift
@@ -8,6 +8,96 @@
import Foundation
import SwiftUI
import GameController
+import MetalKit
+import Metal
+import Darwin
+
+class LogCapture {
+ static let shared = LogCapture()
+
+ private var stdoutPipe: Pipe?
+ private var stderrPipe: Pipe?
+ private let originalStdout: Int32
+ private let originalStderr: Int32
+
+ var capturedLogs: [String] = [] {
+ didSet {
+ DispatchQueue.main.async {
+ NotificationCenter.default.post(name: .newLogCaptured, object: nil)
+ }
+ }
+ }
+
+ private init() {
+ originalStdout = dup(STDOUT_FILENO)
+ originalStderr = dup(STDERR_FILENO)
+ startCapturing()
+ }
+
+ func startCapturing() {
+ stdoutPipe = Pipe()
+ stderrPipe = Pipe()
+
+ redirectOutput(to: stdoutPipe!, fileDescriptor: STDOUT_FILENO)
+ redirectOutput(to: stderrPipe!, fileDescriptor: STDERR_FILENO)
+
+ setupReadabilityHandler(for: stdoutPipe!, isStdout: true)
+ setupReadabilityHandler(for: stderrPipe!, isStdout: false)
+ }
+
+ func stopCapturing() {
+ dup2(originalStdout, STDOUT_FILENO)
+ dup2(originalStderr, STDERR_FILENO)
+
+ stdoutPipe?.fileHandleForReading.readabilityHandler = nil
+ stderrPipe?.fileHandleForReading.readabilityHandler = nil
+ }
+
+ private func redirectOutput(to pipe: Pipe, fileDescriptor: Int32) {
+ dup2(pipe.fileHandleForWriting.fileDescriptor, fileDescriptor)
+ }
+
+ private func setupReadabilityHandler(for pipe: Pipe, isStdout: Bool) {
+ pipe.fileHandleForReading.readabilityHandler = { [weak self] fileHandle in
+ let data = fileHandle.availableData
+ let originalFD = isStdout ? self?.originalStdout : self?.originalStderr
+ write(originalFD ?? STDOUT_FILENO, (data as NSData).bytes, data.count)
+
+ if let logString = String(data: data, encoding: .utf8),
+ let cleanedLog = self?.cleanLog(logString), !cleanedLog.isEmpty {
+ self?.capturedLogs.append(cleanedLog)
+ }
+ }
+ }
+
+ private func cleanLog(_ raw: String) -> String? {
+ let lines = raw.split(separator: "\n")
+ let filteredLines = lines.filter { line in
+ !line.contains("SwiftUI") &&
+ !line.contains("ForEach") &&
+ !line.contains("VStack") &&
+ !line.contains("Invalid frame dimension (negative or non-finite).")
+ }
+
+ let cleaned = filteredLines.map { line -> String in
+ if let tabRange = line.range(of: "\t") {
+ return line[tabRange.upperBound...].trimmingCharacters(in: .whitespacesAndNewlines)
+ }
+ return line.trimmingCharacters(in: .whitespacesAndNewlines)
+ }.joined(separator: "\n")
+
+ return cleaned.isEmpty ? nil : cleaned.replacingOccurrences(of: "\n\n", with: "\n")
+ }
+
+ deinit {
+ stopCapturing()
+ }
+}
+
+
+extension Notification.Name {
+ static let newLogCaptured = Notification.Name("newLogCaptured")
+}
struct Controller: Identifiable, Hashable {
var id: String
@@ -28,60 +118,106 @@ struct iOSNav: View {
}
}
+func threadEntry(_ arg: () -> Void) -> UnsafeMutableRawPointer? {
+ arg()
+ return nil
+}
-class Ryujinx {
- private var isRunning = false
+
+class Ryujinx : ObservableObject {
+ @Published var isRunning = false
let virtualController = VirtualController()
@Published var controllerMap: [Controller] = []
@Published var metalLayer: CAMetalLayer? = nil
+ @Published var isPortrait = false
@Published var firmwareversion = "0"
- @Published var emulationUIView = UIView()
+ @Published var emulationUIView: MeloMTKView? = nil
+ @Published var config: Ryujinx.Arguments? = nil
@Published var games: [Game] = []
@Published var defMLContentSize: CGFloat?
+ var thread: pthread_t? = nil
+
+ @Published var jitenabled = false
+
var shouldMetal: Bool {
metalLayer == nil
}
static let shared = Ryujinx()
-
- private init() {
+
+ func addGames() {
self.games = loadGames()
}
- public struct Configuration : Codable, Equatable {
+ func runloop(_ cool: @escaping () -> Void) {
+ if UserDefaults.standard.bool(forKey: "runOnMainThread") {
+ RunLoop.main.perform {
+ cool()
+ }
+ } else {
+ // Box the closure
+ let boxed = Unmanaged.passRetained(ClosureBox(cool)).toOpaque()
+
+ var thread: pthread_t?
+ let result = pthread_create(&thread, nil, { arg in
+ let unmanaged = Unmanaged.fromOpaque(arg)
+ let box = unmanaged.takeRetainedValue()
+ box.closure()
+ return nil
+ }, boxed)
+
+ if result == 0 {
+ pthread_detach(thread!)
+ } else {
+ print("Failed to create thread: \(result)")
+ Unmanaged.fromOpaque(boxed).release()
+ }
+ }
+ }
+
+ private class ClosureBox {
+ let closure: () -> Void
+ init(_ closure: @escaping () -> Void) {
+ self.closure = closure
+ }
+ }
+
+ public class Arguments : Observable, Codable, Equatable {
var gamepath: String
var inputids: [String]
- var resscale: Float
- var debuglogs: Bool
- var tracelogs: Bool
- var nintendoinput: Bool
- var enableInternet: Bool
- var listinputids: Bool
- var aspectRatio: AspectRatio
- var memoryManagerMode: String
- var disableShaderCache: Bool
- var hypervisor: Bool
- var disableDockedMode: Bool
- var enableTextureRecompression: Bool
- var additionalArgs: [String]
- var maxAnisotropy: Float
- var macroHLE: Bool
- var ignoreMissingServices: Bool
- var expandRam: Bool
- var dfsIntegrityChecks: Bool
- var disablePTC: Bool
- var disablevsync: Bool
- var language: SystemLanguage
- var regioncode: SystemRegionCode
- var handHeldController: Bool
+ var inputDSUServers: [String]
+ var resscale: Float = 1.0
+ var debuglogs: Bool = false
+ var tracelogs: Bool = false
+ var nintendoinput: Bool = true
+ var enableInternet: Bool = false
+ var listinputids: Bool = false
+ var aspectRatio: AspectRatio = .fixed16x9
+ var memoryManagerMode: String = "HostMappedUnsafe"
+ var disableShaderCache: Bool = false
+ var hypervisor: Bool = false
+ var disableDockedMode: Bool = false
+ var enableTextureRecompression: Bool = true
+ var additionalArgs: [String] = []
+ var maxAnisotropy: Float = 1.0
+ var macroHLE: Bool = true
+ var ignoreMissingServices: Bool = false
+ var expandRam: Bool = false
+ var dfsIntegrityChecks: Bool = false
+ var disablePTC: Bool = false
+ var disablevsync: Bool = false
+ var language: SystemLanguage = .americanEnglish
+ var regioncode: SystemRegionCode = .usa
+ var handHeldController: Bool = true
-
- init(gamepath: String,
+
+ init(gamepath: String = "",
inputids: [String] = [],
+ inputDSUServers: [String] = [],
debuglogs: Bool = false,
tracelogs: Bool = false,
listinputids: Bool = false,
@@ -104,10 +240,11 @@ class Ryujinx {
disablevsync: Bool = false,
language: SystemLanguage = .americanEnglish,
regioncode: SystemRegionCode = .usa,
- handHeldController: Bool = false
+ handHeldController: Bool = false,
) {
self.gamepath = gamepath
self.inputids = inputids
+ self.inputDSUServers = inputDSUServers
self.debuglogs = debuglogs
self.tracelogs = tracelogs
self.listinputids = listinputids
@@ -132,17 +269,73 @@ class Ryujinx {
self.regioncode = regioncode
self.handHeldController = handHeldController
}
+
+
+ static func == (lhs: Arguments, rhs: Arguments) -> Bool {
+ return lhs.resscale == rhs.resscale &&
+ lhs.debuglogs == rhs.debuglogs &&
+ lhs.tracelogs == rhs.tracelogs &&
+ lhs.nintendoinput == rhs.nintendoinput &&
+ lhs.enableInternet == rhs.enableInternet &&
+ lhs.listinputids == rhs.listinputids &&
+ lhs.aspectRatio == rhs.aspectRatio &&
+ lhs.memoryManagerMode == rhs.memoryManagerMode &&
+ lhs.disableShaderCache == rhs.disableShaderCache &&
+ lhs.hypervisor == rhs.hypervisor &&
+ lhs.disableDockedMode == rhs.disableDockedMode &&
+ lhs.enableTextureRecompression == rhs.enableTextureRecompression &&
+ lhs.additionalArgs == rhs.additionalArgs &&
+ lhs.maxAnisotropy == rhs.maxAnisotropy &&
+ lhs.macroHLE == rhs.macroHLE &&
+ lhs.ignoreMissingServices == rhs.ignoreMissingServices &&
+ lhs.expandRam == rhs.expandRam &&
+ lhs.dfsIntegrityChecks == rhs.dfsIntegrityChecks &&
+ lhs.disablePTC == rhs.disablePTC &&
+ lhs.disablevsync == rhs.disablevsync &&
+ lhs.language == rhs.language &&
+ lhs.regioncode == rhs.regioncode &&
+ lhs.handHeldController == rhs.handHeldController
+ }
}
- func start(with config: Configuration) throws {
+ func start(with config: Arguments) throws {
guard !isRunning else {
throw RyujinxError.alreadyRunning
}
- isRunning = true
+ self.config = config
- RunLoop.current.perform {
+
+ if UserDefaults.standard.bool(forKey: "lockInApp") {
+ let cool = Thread {
+ while true {
+ if UserDefaults.standard.bool(forKey: "lockInApp") {
+ if let workspaceClass = NSClassFromString("LSApplicationWorkspace") as? NSObject.Type,
+ let workspace = workspaceClass.perform(NSSelectorFromString("defaultWorkspace"))?.takeUnretainedValue() {
+
+ let selector = NSSelectorFromString("openApplicationWithBundleID:")
+
+ if workspace.responds(to: selector) {
+ workspace.perform(selector, with: Bundle.main.bundleIdentifier ?? "")
+ } else {
+ print("Selector not found or not responding.")
+ }
+ } else {
+ print("Could not get LSApplicationWorkspace class.")
+ }
+ }
+ }
+ }
+
+ cool.qualityOfService = .userInteractive
+ cool.start()
+ }
+
+
+ runloop { [self] in
+
+ isRunning = true
let url = URL(string: config.gamepath)
@@ -156,36 +349,133 @@ class Ryujinx {
var argvPtrs = cArgs
// Start the emulation
- let result = main_ryujinx_sdl(Int32(args.count), &argvPtrs)
-
- if result != 0 {
- self.isRunning = false
- if let accessing, accessing {
- url!.stopAccessingSecurityScopedResource()
- }
+ if isRunning {
+ let result = main_ryujinx_sdl(Int32(args.count), &argvPtrs)
- throw RyujinxError.executionError(code: result)
+ if result != 0 {
+ DispatchQueue.main.async {
+ self.isRunning = false
+ }
+ if let accessing, accessing {
+ url!.stopAccessingSecurityScopedResource()
+ }
+
+ throw RyujinxError.executionError(code: result)
+ }
}
} catch {
- self.isRunning = false
- Self.log("Emulation failed to start: \(error)")
+ DispatchQueue.main.async {
+ self.isRunning = false
+ }
+ Thread.sleep(forTimeInterval: 0.3)
+ let logs = LogCapture.shared.capturedLogs
+ let parsedLogs = extractExceptionInfo(logs)
+ if let parsedLogs {
+ DispatchQueue.main.async {
+ let result = Array(logs.suffix(from: parsedLogs.lineIndex))
+
+ LogCapture.shared.capturedLogs = Array(LogCapture.shared.capturedLogs.prefix(upTo: parsedLogs.lineIndex))
+ let dateFormatter = DateFormatter()
+ dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
+ let currentDate = Date()
+ let dateString = dateFormatter.string(from: currentDate)
+ let path = URL.documentsDirectory.appendingPathComponent("StackTrace").appendingPathComponent("StackTrace-\(dateString).txt").path
+
+ self.saveArrayAsTextFile(strings: result, filePath: path)
+
+
+ presentAlert(title: "MeloNX Crashed!", message: parsedLogs.exceptionType + ": " + parsedLogs.message) {
+ UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
+ exit(0)
+ }
+ }
+ }
+ } else {
+ DispatchQueue.main.async {
+ presentAlert(title: "MeloNX Crashed!", message: "Unknown Error") {
+ UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
+ exit(0)
+ }
+ }
+ }
+ }
}
}
}
+
+ func saveArrayAsTextFile(strings: [String], filePath: String) {
+ let text = strings.joined(separator: "\n")
+
+ let path = URL.documentsDirectory.appendingPathComponent("StackTrace").path
+
+ do {
+ try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: false)
+ } catch {
+
+ }
+
+ do {
+ try text.write(to: URL(fileURLWithPath: filePath), atomically: true, encoding: .utf8)
+ print("File saved successfully.")
+ } catch {
+ print("Error saving file: \(error)")
+ }
+ }
+
+ struct ExceptionInfo {
+ let exceptionType: String
+ let message: String
+ let lineIndex: Int
+ }
+ func extractExceptionInfo(_ logs: [String]) -> ExceptionInfo? {
+ for i in (0.. [Game] {
let fileManager = FileManager.default
@@ -197,7 +487,7 @@ class Ryujinx {
do {
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
} catch {
- print("Failed to create roms directory: \(error)")
+ // print("Failed to create roms directory: \(error)")
}
}
var games: [Game] = []
@@ -222,19 +512,18 @@ class Ryujinx {
games.append(game)
} catch {
- print(error)
+ // print(error)
}
}
return games
} catch {
- print("Error loading games from roms folder: \(error)")
+ // print("Error loading games from roms folder: \(error)")
return games
}
-
}
- private func buildCommandLineArgs(from config: Configuration) -> [String] {
+ func buildCommandLineArgs(from config: Arguments) -> [String] {
var args: [String] = []
// Add the game path
@@ -246,18 +535,41 @@ class Ryujinx {
args.append(contentsOf: ["--memory-manager-mode", config.memoryManagerMode])
- // args.append(contentsOf: ["--exclusive-fullscreen", String(true)])
- // args.append(contentsOf: ["--exclusive-fullscreen-width", "\(Int(UIScreen.main.bounds.width))"])
- // args.append(contentsOf: ["--exclusive-fullscreen-height", "\(Int(UIScreen.main.bounds.height))"])
+ args.append(contentsOf: ["--exclusive-fullscreen", String(true)])
+ args.append(contentsOf: ["--exclusive-fullscreen-width", "\(Int(UIScreen.main.bounds.width))"])
+ args.append(contentsOf: ["--exclusive-fullscreen-height", "\(Int(UIScreen.main.bounds.height))"])
// We don't need this. Ryujinx should handle it fine :3
// this also causes crashes in some games :3
+ var model = ""
+
+ var systemInfo = utsname()
+ uname(&systemInfo)
+ let machineMirror = Mirror(reflecting: systemInfo.machine)
+ model = machineMirror.children.reduce("") { identifier, element in
+ guard let value = element.value as? Int8, value != 0 else { return identifier }
+ return identifier + String(UnicodeScalar(UInt8(value)))
+ }
+
+ args.append(contentsOf: ["--device-model", model])
+
+ args.append(contentsOf: ["--device-display-name", UIDevice.modelName])
+
+ if checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") {
+ args.append("--has-memory-entitlement")
+ }
+
args.append(contentsOf: ["--system-language", config.language.rawValue])
args.append(contentsOf: ["--system-region", config.regioncode.rawValue])
args.append(contentsOf: ["--aspect-ratio", config.aspectRatio.rawValue])
+ args.append(contentsOf: ["--system-timezone", TimeZone.current.identifier])
+
+ // args.append(contentsOf: ["--system-time-offset", String(TimeZone.current.secondsFromGMT())])
+
+
if config.nintendoinput {
args.append("--correct-controller")
}
@@ -334,26 +646,37 @@ class Ryujinx {
}
}
}
-
- // Apped any additional arguments
+
+ // Append the input dsu servers (limit to 8 (used to be 4) just in case)
+ if !config.inputDSUServers.isEmpty {
+ config.inputDSUServers.prefix(8).enumerated().forEach { index, inputDSUServer in
+ if index == 0 {
+ args.append(contentsOf: ["--input-dsu-server-handheld", inputDSUServer])
+ }
+ args.append(contentsOf: ["--input-dsu-server-\(index + 1)", inputDSUServer])
+ }
+ }
+
args.append(contentsOf: config.additionalArgs)
return args
}
+ func checkIfKeysImported() -> Bool {
+ let keysDirectory = URL.documentsDirectory.appendingPathComponent("system")
+ let keysFile = keysDirectory.appendingPathComponent("prod.keys")
+
+ return FileManager.default.fileExists(atPath: keysFile.path)
+ }
+
func fetchFirmwareVersion() -> String {
- do {
- let firmwareVersionPointer = installed_firmware_version()
- if let pointer = firmwareVersionPointer {
- let firmwareVersion = String(cString: pointer)
- DispatchQueue.main.async {
- self.firmwareversion = firmwareVersion
- }
- return firmwareVersion
+ let firmwareVersionPointer = installed_firmware_version()
+ if let pointer = firmwareVersionPointer {
+ let firmwareVersion = String(cString: pointer)
+ DispatchQueue.main.async {
+ self.firmwareversion = firmwareVersion
}
-
- } catch {
- print(error)
+ return firmwareVersion
}
return "0"
@@ -361,7 +684,7 @@ class Ryujinx {
func installFirmware(firmwarePath: String) {
guard let cString = firmwarePath.cString(using: .utf8) else {
- print("Invalid firmware path")
+ // print("Invalid firmware path")
return
}
@@ -377,12 +700,12 @@ class Ryujinx {
guard let titleIdCString = titleId.cString(using: .utf8),
let pathCString = path.cString(using: .utf8)
else {
- print("Invalid path")
+ // print("Invalid path")
return []
}
let listPointer = get_dlc_nca_list(titleIdCString, pathCString)
- print("DLC parcing success: \(listPointer.success)")
+ // print("DLC parcing success: \(listPointer.success)")
guard listPointer.success else { return [] }
let list = Array(UnsafeBufferPointer(start: listPointer.items, count: Int(listPointer.size)))
@@ -434,7 +757,7 @@ class Ryujinx {
let guid = generateGamepadId(joystickIndex: i)
let name = String(cString: SDL_GameControllerName(controller))
- print("Controller \(i): \(name), GUID: \(guid ?? "")")
+ // print("Controller \(i): \(name), GUID: \(guid ?? "")")
guard let guid else {
SDL_GameControllerClose(controller)
@@ -465,88 +788,163 @@ class Ryujinx {
do {
if fileManager.fileExists(atPath: registeredFolder) {
try fileManager.removeItem(atPath: registeredFolder)
- print("Folder removed successfully.")
+ // print("Folder removed successfully.")
let version = fetchFirmwareVersion()
if version.isEmpty {
self.firmwareversion = "0"
} else {
- print("Firmware eeeeee \(version)")
+ // print("Firmware eeeeee \(version)")
}
} else {
- print("Folder does not exist.")
+ // print("Folder does not exist.")
}
} catch {
- print("Error removing folder: \(error)")
+ // print("Error removing folder: \(error)")
}
}
-
- func repeatuntilfindLayer() {
- Task { @MainActor in
- while self.metalLayer == nil {
- let layer = self.getMetalLayer(nil)
-
- if layer != nil {
- DispatchQueue.main.async {
- self.metalLayer = layer
- }
- self.metalLayer = layer
- break
- }
-
- Thread.sleep(forTimeInterval: 0.1)
- try await Task.sleep(nanoseconds: 100_000_000)
- }
- }
- }
-
-
- @MainActor
- func getMetalLayer(_ window: OpaquePointer?) -> CAMetalLayer? {
- var window = window
- if window == nil {
- window = SDL_GetWindowFromID(1)
- }
-
- var windowInfo = SDL_SysWMinfo()
- SDL_GetWindowWMInfo(window, &windowInfo)
-
-
- guard let uiWindow = windowInfo.info.uikit.window,
- let rootView = uiWindow.takeUnretainedValue().rootViewController?.view else {
- print("Unable to get root view")
- return nil
- }
-
- func findMetalLayer(in view: UIView) -> CAMetalLayer? {
- if let metalLayer = view.layer as? CAMetalLayer {
- return metalLayer
- }
-
- for subview in view.subviews {
- if let metalLayer = findMetalLayer(in: subview) {
- return metalLayer
- }
- }
-
- return nil
- }
-
- if let existingLayer = findMetalLayer(in: rootView) {
- print("Found Metal Layer")
- return existingLayer
- }
- print("found nothing")
- return nil
- }
-
static func log(_ message: String) {
- print("[Ryujinx] \(message)")
+ // print("[Ryujinx] \(message)")
+ }
+
+ public func updateOrientation() -> Bool {
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+ let window = windowScene.windows.first {
+ return (window.bounds.size.height > window.bounds.size.width)
+ }
+ return false
+ }
+
+ func ryuIsJITEnabled() {
+ jitenabled = isJITEnabled()
}
}
+public extension UIDevice {
+ static let modelName: String = {
+ var systemInfo = utsname()
+ uname(&systemInfo)
+ let machineMirror = Mirror(reflecting: systemInfo.machine)
+ let identifier = machineMirror.children.reduce("") { identifier, element in
+ guard let value = element.value as? Int8, value != 0 else { return identifier }
+ return identifier + String(UnicodeScalar(UInt8(value)))
+ }
+
+ func mapToDevice(identifier: String) -> String { // swiftlint:disable:this cyclomatic_complexity
+ #if os(iOS)
+ switch identifier {
+ case "iPod5,1": return "iPod touch (5th generation)"
+ case "iPod7,1": return "iPod touch (6th generation)"
+ case "iPod9,1": return "iPod touch (7th generation)"
+ case "iPhone3,1", "iPhone3,2", "iPhone3,3": return "iPhone 4"
+ case "iPhone4,1": return "iPhone 4s"
+ case "iPhone5,1", "iPhone5,2": return "iPhone 5"
+ case "iPhone5,3", "iPhone5,4": return "iPhone 5c"
+ case "iPhone6,1", "iPhone6,2": return "iPhone 5s"
+ case "iPhone7,2": return "iPhone 6"
+ case "iPhone7,1": return "iPhone 6 Plus"
+ case "iPhone8,1": return "iPhone 6s"
+ case "iPhone8,2": return "iPhone 6s Plus"
+ case "iPhone9,1", "iPhone9,3": return "iPhone 7"
+ case "iPhone9,2", "iPhone9,4": return "iPhone 7 Plus"
+ case "iPhone10,1", "iPhone10,4": return "iPhone 8"
+ case "iPhone10,2", "iPhone10,5": return "iPhone 8 Plus"
+ case "iPhone10,3", "iPhone10,6": return "iPhone X"
+ case "iPhone11,2": return "iPhone XS"
+ case "iPhone11,4", "iPhone11,6": return "iPhone XS Max"
+ case "iPhone11,8": return "iPhone XR"
+ case "iPhone12,1": return "iPhone 11"
+ case "iPhone12,3": return "iPhone 11 Pro"
+ case "iPhone12,5": return "iPhone 11 Pro Max"
+ case "iPhone13,1": return "iPhone 12 mini"
+ case "iPhone13,2": return "iPhone 12"
+ case "iPhone13,3": return "iPhone 12 Pro"
+ case "iPhone13,4": return "iPhone 12 Pro Max"
+ case "iPhone14,4": return "iPhone 13 mini"
+ case "iPhone14,5": return "iPhone 13"
+ case "iPhone14,2": return "iPhone 13 Pro"
+ case "iPhone14,3": return "iPhone 13 Pro Max"
+ case "iPhone14,7": return "iPhone 14"
+ case "iPhone14,8": return "iPhone 14 Plus"
+ case "iPhone15,2": return "iPhone 14 Pro"
+ case "iPhone15,3": return "iPhone 14 Pro Max"
+ case "iPhone15,4": return "iPhone 15"
+ case "iPhone15,5": return "iPhone 15 Plus"
+ case "iPhone16,1": return "iPhone 15 Pro"
+ case "iPhone16,2": return "iPhone 15 Pro Max"
+ case "iPhone17,3": return "iPhone 16"
+ case "iPhone17,4": return "iPhone 16 Plus"
+ case "iPhone17,1": return "iPhone 16 Pro"
+ case "iPhone17,2": return "iPhone 16 Pro Max"
+ case "iPhone17,5": return "iPhone 16e"
+ case "iPhone8,4": return "iPhone SE"
+ case "iPhone12,8": return "iPhone SE (2nd generation)"
+ case "iPhone14,6": return "iPhone SE (3rd generation)"
+ case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4": return "iPad 2"
+ case "iPad3,1", "iPad3,2", "iPad3,3": return "iPad (3rd generation)"
+ case "iPad3,4", "iPad3,5", "iPad3,6": return "iPad (4th generation)"
+ case "iPad6,11", "iPad6,12": return "iPad (5th generation)"
+ case "iPad7,5", "iPad7,6": return "iPad (6th generation)"
+ case "iPad7,11", "iPad7,12": return "iPad (7th generation)"
+ case "iPad11,6", "iPad11,7": return "iPad (8th generation)"
+ case "iPad12,1", "iPad12,2": return "iPad (9th generation)"
+ case "iPad13,18", "iPad13,19": return "iPad (10th generation)"
+ case "iPad4,1", "iPad4,2", "iPad4,3": return "iPad Air"
+ case "iPad5,3", "iPad5,4": return "iPad Air 2"
+ case "iPad11,3", "iPad11,4": return "iPad Air (3rd generation)"
+ case "iPad13,1", "iPad13,2": return "iPad Air (4th generation)"
+ case "iPad13,16", "iPad13,17": return "iPad Air (5th generation)"
+ case "iPad14,8", "iPad14,9": return "iPad Air (11-inch) (M2)"
+ case "iPad14,10", "iPad14,11": return "iPad Air (13-inch) (M2)"
+ case "iPad2,5", "iPad2,6", "iPad2,7": return "iPad mini"
+ case "iPad4,4", "iPad4,5", "iPad4,6": return "iPad mini 2"
+ case "iPad4,7", "iPad4,8", "iPad4,9": return "iPad mini 3"
+ case "iPad5,1", "iPad5,2": return "iPad mini 4"
+ case "iPad11,1", "iPad11,2": return "iPad mini (5th generation)"
+ case "iPad14,1", "iPad14,2": return "iPad mini (6th generation)"
+ case "iPad16,1", "iPad16,2": return "iPad mini (A17 Pro)"
+ case "iPad6,3", "iPad6,4": return "iPad Pro (9.7-inch)"
+ case "iPad7,3", "iPad7,4": return "iPad Pro (10.5-inch)"
+ case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": return "iPad Pro (11-inch) (1st generation)"
+ case "iPad8,9", "iPad8,10": return "iPad Pro (11-inch) (2nd generation)"
+ case "iPad13,4", "iPad13,5", "iPad13,6", "iPad13,7": return "iPad Pro (11-inch) (3rd generation)"
+ case "iPad14,3", "iPad14,4": return "iPad Pro (11-inch) (4th generation)"
+ case "iPad16,3", "iPad16,4": return "iPad Pro (11-inch) (M4)"
+ case "iPad6,7", "iPad6,8": return "iPad Pro (12.9-inch) (1st generation)"
+ case "iPad7,1", "iPad7,2": return "iPad Pro (12.9-inch) (2nd generation)"
+ case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8": return "iPad Pro (12.9-inch) (3rd generation)"
+ case "iPad8,11", "iPad8,12": return "iPad Pro (12.9-inch) (4th generation)"
+ case "iPad13,8", "iPad13,9", "iPad13,10", "iPad13,11":return "iPad Pro (12.9-inch) (5th generation)"
+ case "iPad14,5", "iPad14,6": return "iPad Pro (12.9-inch) (6th generation)"
+ case "iPad16,5", "iPad16,6": return "iPad Pro (13-inch) (M4)"
+ case "AppleTV5,3": return "Apple TV"
+ case "AppleTV6,2": return "Apple TV 4K"
+ case "AudioAccessory1,1": return "HomePod"
+ case "AudioAccessory5,1": return "HomePod mini"
+ case "i386", "x86_64", "arm64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS"))"
+ default: return identifier
+ }
+ #elseif os(tvOS)
+ switch identifier {
+ case "AppleTV5,3": return "Apple TV 4"
+ case "AppleTV6,2", "AppleTV11,1", "AppleTV14,1": return "Apple TV 4K"
+ case "i386", "x86_64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "tvOS"))"
+ default: return identifier
+ }
+ #elseif os(visionOS)
+ switch identifier {
+ case "RealityDevice14,1": return "Apple Vision Pro"
+ default: return identifier
+ }
+ #endif
+ }
+
+ return mapToDevice(identifier: identifier)
+ }()
+
+}
diff --git a/src/MeloNX/MeloNX/App/Intents/LaunchGameIntent.swift b/src/MeloNX/MeloNX/App/Intents/LaunchGameIntent.swift
index d6d314aae..33b8407e2 100644
--- a/src/MeloNX/MeloNX/App/Intents/LaunchGameIntent.swift
+++ b/src/MeloNX/MeloNX/App/Intents/LaunchGameIntent.swift
@@ -32,10 +32,10 @@ struct LaunchGameIntentDef: AppIntent {
let ryujinx = Ryujinx.shared.games
- let name = findClosestGameName(input: gameName, games: ryujinx.flatMap(\.titleName))
+ let name = findClosestGameName(input: gameName, games: ryujinx.compactMap(\.titleName))
let urlString = "melonx://game?name=\(name ?? gameName)"
- print(urlString)
+ // print(urlString)
if let url = URL(string: urlString) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
diff --git a/src/MeloNX/MeloNX/App/Models/Game.swift b/src/MeloNX/MeloNX/App/Models/Game.swift
index 96d27591d..af5751e44 100644
--- a/src/MeloNX/MeloNX/App/Models/Game.swift
+++ b/src/MeloNX/MeloNX/App/Models/Game.swift
@@ -57,28 +57,21 @@ public struct Game: Identifiable, Equatable, Hashable {
gameTemp.icon = UIImage(data: imageData)
} else {
- print("Invalid image size.")
+ // print("Invalid image size.")
}
return gameTemp
}
func createImage(from gameInfo: GameInfo) -> UIImage? {
- // Access the struct
let gameInfoValue = gameInfo
- // Get the image data
let imageSize = Int(gameInfoValue.ImageSize)
guard imageSize > 0, imageSize <= 1024 * 1024 else {
- print("Invalid image size.")
+ // print("Invalid image size.")
return nil
}
- // Convert the ImageData byte array to Swift's Data
let imageData = Data(bytes: gameInfoValue.ImageData, count: imageSize)
-
- // Create a UIImage (or NSImage on macOS)
- print(imageData)
-
return UIImage(data: imageData)
}
}
diff --git a/src/MeloNX/MeloNX/App/Models/LatestVersionResponse.swift b/src/MeloNX/MeloNX/App/Models/LatestVersionResponse.swift
new file mode 100644
index 000000000..8ffbffc36
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Models/LatestVersionResponse.swift
@@ -0,0 +1,38 @@
+//
+// LatestVersionResponse.swift
+// MeloNX
+//
+// Created by Bella on 12/03/2025.
+//
+
+
+struct LatestVersionResponse: Codable {
+ let version_number: String
+ let version_number_stripped: String
+ let changelog: String
+ let download_link: String
+
+ #if DEBUG
+ static let example1 = LatestVersionResponse(
+ version_number: "1.0.0",
+ version_number_stripped: "100",
+ changelog: """
+ - Rewrite Display Code (SDL isn't used for display anymore)
+ - Add New Onboarding / Setup
+ - Better Performance
+ - Remove "SDL Window" option in settings
+ - Fix JIT Cache Regions
+ - Fix how JIT is detected in Settings
+ - Fix ABYX being swapped on controller.
+ - Settings are now a config.json file
+ - Fix Performance Overlay not showing when Virtual Controller is hidden
+ - Add displaying logs when Loading or in-game
+ - Fix Launching games from outside of the roms folder
+ - Add Waiting for JIT popup
+ - Fix spesific Games
+ - Added Back Herobrine ("You were supposed to be the hero, Bryan")
+ """,
+ download_link: "https://example.com"
+ )
+ #endif
+}
\ No newline at end of file
diff --git a/src/MeloNX/MeloNX/App/Models/ToggleButtonsState.swift b/src/MeloNX/MeloNX/App/Models/ToggleButtonsState.swift
new file mode 100644
index 000000000..08e6d9310
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Models/ToggleButtonsState.swift
@@ -0,0 +1,27 @@
+//
+// 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)
+}
diff --git a/src/MeloNX/MeloNX/App/Views/ContentView.swift b/src/MeloNX/MeloNX/App/Views/ContentView.swift
deleted file mode 100644
index cce781157..000000000
--- a/src/MeloNX/MeloNX/App/Views/ContentView.swift
+++ /dev/null
@@ -1,360 +0,0 @@
-//
-// ContentView.swift
-// MeloNX
-//
-// Created by Stossy11 on 3/11/2024.
-//
-
-import SwiftUI
-// import SDL2
-import GameController
-import Darwin
-import UIKit
-import MetalKit
-// import SDL
-
-struct MoltenVKSettings: Codable, Hashable {
- let string: String
- var value: String
-}
-
-struct ContentView: View {
- // Games
- @State private var game: Game?
-
- // Controllers
- @State private var controllersList: [Controller] = []
- @State private var currentControllers: [Controller] = []
- @State var onscreencontroller: Controller = Controller(id: "", name: "")
- @State var nativeControllers: [GCController: NativeController] = [:]
- @State private var isVirtualControllerActive: Bool = false
- @AppStorage("isVirtualController") var isVCA: Bool = true
-
- // Settings and Configuration
- @State private var config: Ryujinx.Configuration
- @State var settings: [MoltenVKSettings]
- @AppStorage("useTrollStore") var useTrollStore: Bool = false
-
- // JIT
- @AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
-
- // Other Configuration
- @State var isMK8: Bool = false
- @AppStorage("quit") var quit: Bool = false
- @State var quits: Bool = false
- @AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = true
- @AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = true
-
- // Loading Animation
- @State private var clumpOffset: CGFloat = -100
- private let clumpWidth: CGFloat = 100
- private let animationDuration: Double = 1.0
- @State private var isAnimating = false
- @State var isLoading = true
-
- // MARK: - Initialization
- init() {
- let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "")
- _config = State(initialValue: defaultConfig)
-
- let defaultSettings: [MoltenVKSettings] = [ // Default MoltenVK Settings.
- MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"),
- MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "1"),
- MoltenVKSettings(string: "MVK_DEBUG", value: "0"),
- MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "0"),
- // Uses more ram but makes performance higher, may add an option in settings to change or enable / disable this value (default 64)
- MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "128"),
- ]
-
- _settings = State(initialValue: defaultSettings)
-
- initializeSDL()
- }
-
- // MARK: - Body
- var body: some View {
- if game != nil, quits == false {
- if isLoading {
- if Air.shared.connected {
- Text("")
- .onAppear() {
- Air.play(AnyView(emulationView))
- }
- } else {
- ZStack {
- emulationView
- }
- }
- } else {
- // This is when the game starts to stop the animation
- if #available(iOS 16, *) {
- EmulationView()
- .persistentSystemOverlays(.hidden)
- .onAppear() {
- isAnimating = false
- }
- } else {
- EmulationView()
- .onAppear() {
- isAnimating = false
- }
- }
- }
- } else {
- // This is the main menu view that includes the Settings and the Game Selector
- mainMenuView
- .onAppear() {
- quits = false
-
- initControllerObservers() // This initializes the Controller Observers that refreshes the controller list when a new controller connecvts.
- }
- .onOpenURL() { url in
- if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
- components.host == "game" {
- if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
-
- game = Ryujinx.shared.games.first(where: { $0.titleId == text })
- } else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
- game = Ryujinx.shared.games.first(where: { $0.titleName == text })
- }
- }
- }
- }
-
- }
-
-
- private func initControllerObservers() {
- NotificationCenter.default.addObserver(
- forName: .GCControllerDidConnect,
- object: nil,
- queue: .main) { notification in
- if let controller = notification.object as? GCController {
- print("Controller connected: \(controller.productCategory)")
- nativeControllers[controller] = .init(controller)
- refreshControllersList()
- }
- }
-
-
- NotificationCenter.default.addObserver(
- forName: .GCControllerDidDisconnect,
- object: nil,
- queue: .main) { notification in
- if let controller = notification.object as? GCController {
- print("Controller disconnected: \(controller.productCategory)")
- nativeControllers[controller]?.cleanup()
- nativeControllers[controller] = nil
- refreshControllersList()
- }
- }
- }
-
- // MARK: - View Components
- private var emulationView: some View {
- GeometryReader { screenGeometry in
- ZStack {
- HStack(spacing: screenGeometry.size.width * 0.04) {
- if let icon = game?.icon {
- Image(uiImage: icon)
- .resizable()
- .frame(
- width: min(screenGeometry.size.width * 0.25, 250),
- height: min(screenGeometry.size.width * 0.25, 250)
- )
- .clipShape(RoundedRectangle(cornerRadius: 16))
- .shadow(color: .black.opacity(0.5), radius: 10, x: 0, y: 5)
- }
-
- VStack(alignment: .leading, spacing: screenGeometry.size.height * 0.015) {
- Text("Loading \(game?.titleName ?? "Game")")
- .font(.system(size: min(screenGeometry.size.width * 0.04, 32)))
- .foregroundColor(.white)
-
- GeometryReader { geometry in
- let containerWidth = min(screenGeometry.size.width * 0.35, 350)
-
- ZStack(alignment: .leading) {
- Rectangle()
- .cornerRadius(10)
- .frame(width: containerWidth, height: min(screenGeometry.size.height * 0.015, 12))
- .foregroundColor(.gray.opacity(0.3))
- .shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2)
-
- Rectangle()
- .cornerRadius(10)
- .frame(width: clumpWidth, height: min(screenGeometry.size.height * 0.015, 12))
- .foregroundColor(.blue)
- .shadow(color: .blue.opacity(0.5), radius: 4, x: 0, y: 2)
- .offset(x: isAnimating ? containerWidth : -clumpWidth)
- .animation(
- Animation.linear(duration: 1.0)
- .repeatForever(autoreverses: false),
- value: isAnimating
- )
- }
- .clipShape(RoundedRectangle(cornerRadius: 16))
- .onAppear {
- isAnimating = true
-
- setupEmulation()
-
-
- Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
- if get_current_fps() != 0 {
- withAnimation {
- isLoading = false
- }
-
- isAnimating = false
- timer.invalidate()
- }
- print(get_current_fps())
- }
- }
- }
- .frame(height: min(screenGeometry.size.height * 0.015, 12))
- .frame(width: min(screenGeometry.size.width * 0.35, 350))
- }
- }
- .padding(.horizontal, screenGeometry.size.width * 0.06)
- .padding(.vertical, screenGeometry.size.height * 0.05)
- .position(
- x: screenGeometry.size.width / 2,
- y: screenGeometry.size.height * 0.5
- )
- }
- }
- }
-
- private var mainMenuView: some View {
- MainTabView(startemu: $game, config: $config, MVKconfig: $settings, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
- .onAppear() {
- Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { timer in
- refreshControllersList()
- }
-
- Air.play(AnyView(
- VStack {
- Image(systemName: "gamecontroller")
- .font(.system(size: 300))
- .foregroundColor(.gray)
- .padding(.bottom, 10)
-
- Text("Select Game")
- .font(.system(size: 150))
- .bold()
- }
- ))
-
- let isJIT = isJITEnabled()
- if !isJIT {
- useTrollStore ? askForJIT() : enableJITEB()
- }
- }
- }
-
- // MARK: - Helper Methods
- var SdlInitFlags: uint = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO; // Initialises SDL2 for Events, Game Controller, Joystick, Audio and Video.
- private func initializeSDL() {
- setMoltenVKSettings()
- SDL_SetMainReady() // Sets SDL Ready
- SDL_iPhoneSetEventPump(SDL_TRUE) // Set iOS Event Pump to true
- SDL_Init(SdlInitFlags) // Initialises SDL2
- initialize()
- }
-
- private func setupEmulation() {
- patchMakeKeyAndVisible()
- isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil)
-
- DispatchQueue.main.async {
- start(displayid: 1)
- }
- }
-
- private func refreshControllersList() {
- controllersList = Ryujinx.shared.getConnectedControllers()
-
- if let onscreen = controllersList.first(where: { $0.name == Ryujinx.shared.virtualController.controllername }) {
- self.onscreencontroller = onscreen
- }
-
- controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) })
- controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") }
-
- currentControllers = []
-
- if controllersList.count == 1 {
- let controller = controllersList[0]
- currentControllers.append(controller)
- } else if (controllersList.count - 1) >= 1 {
- for controller in controllersList {
- if controller.id != onscreencontroller.id && !currentControllers.contains(where: { $0.id == controller.id }) {
- currentControllers.append(controller)
- }
- }
- }
- }
-
-
-
- private func start(displayid: UInt32) {
- guard let game else { return }
-
- config.gamepath = game.fileURL.path
- config.inputids = Array(Set(currentControllers.map(\.id)))
-
- if mVKPreFillBuffer {
- let setting = MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "2")
- setenv(setting.string, setting.value, 1)
- }
-
- if syncqsubmits {
- let setting = MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "2")
- setenv(setting.string, setting.value, 1)
- }
-
- if config.inputids.isEmpty {
- config.inputids.append("0")
- }
-
- do {
- try Ryujinx.shared.start(with: config)
- } catch {
- print("Error: \(error.localizedDescription)")
- }
- }
-
-
-
- // Sets MoltenVK Environment Variables
- private func setMoltenVKSettings() {
- settings.forEach { setting in
- setenv(setting.string, setting.value, 1)
- }
- }
-}
-
-// MARK: - Helper Functions
-func loadSettings() -> Ryujinx.Configuration? {
- guard let jsonString = UserDefaults.standard.string(forKey: "config"),
- let data = jsonString.data(using: .utf8) else {
- return nil
- }
-
- do {
- return try JSONDecoder().decode(Ryujinx.Configuration.self, from: data)
- } catch {
- print("Failed to load settings: \(error)")
- return nil
- }
-}
-
-extension Array {
- @inlinable public mutating func mutableForEach(_ body: (inout Element) throws -> Void) rethrows {
- for index in self.indices {
- try body(&self[index])
- }
- }
-}
diff --git a/src/MeloNX/MeloNX/App/Views/ControllerView/ControllerView.swift b/src/MeloNX/MeloNX/App/Views/ControllerView/ControllerView.swift
deleted file mode 100644
index a11f2f484..000000000
--- a/src/MeloNX/MeloNX/App/Views/ControllerView/ControllerView.swift
+++ /dev/null
@@ -1,324 +0,0 @@
-//
-// ControllerView.swift
-// Pomelo-V2
-//
-// Created by Stossy11 on 16/7/2024.
-//
-
-import SwiftUI
-import GameController
-import SwiftUIJoystick
-import CoreMotion
-
-struct ControllerView: View {
-
- @AppStorage("performacehud") var performacehud: Bool = false
- @AppStorage("quit") var quit: Bool = false
- var body: some View {
- GeometryReader { geometry in
- if geometry.size.height > geometry.size.width && UIDevice.current.userInterfaceIdiom != .pad {
- VStack {
- if performacehud {
- HStack {
-
- PerformanceOverlayView()
-
- Spacer()
-
- // Button("Stop emulation") {
- // DispatchQueue.main.async {
- // stop_emulation()
- // quit = true
- // }
- // }
- }
- }
-
-
- Spacer()
- VStack {
- HStack {
- VStack {
- ShoulderButtonsViewLeft()
- ZStack {
- Joystick()
- DPadView()
- }
- }
- Spacer()
- VStack {
- ShoulderButtonsViewRight()
- ZStack {
- Joystick(iscool: true) // hope this works
- ABXYView()
- }
- }
- }
-
- HStack {
- ButtonView(button: .start) // Adding the + button
- .padding(.horizontal, 40)
- ButtonView(button: .back) // Adding the - button
- .padding(.horizontal, 40)
- }
- }
- }
-
- } else {
- // could be landscape
- VStack {
- if performacehud {
- HStack {
- PerformanceOverlayView()
-
- Spacer()
-
- // Button("Stop emulation") {
- // DispatchQueue.main.async {
- // stop_emulation()
- // quit = true
- // }
- // }
- }
- }
-
-
- Spacer()
- VStack {
- HStack {
-
- // gotta fuckin add + and - now
- VStack {
- ShoulderButtonsViewLeft()
- ZStack {
- Joystick()
- DPadView()
- }
- }
- HStack {
- // Spacer()
- VStack {
- // Spacer()
- ButtonView(button: .back) // Adding the - button
- }
- Spacer()
- VStack {
- // Spacer()
- ButtonView(button: .start) // Adding the + button
- }
- // Spacer()
- }
- VStack {
- ShoulderButtonsViewRight()
- ZStack {
- Joystick(iscool: true) // hope this work s
- ABXYView()
- }
- }
- }
-
- }
- // .padding(.bottom, geometry.size.height / 11) // also extremally broken (
- }
- }
- }
- .padding()
- }
-}
-
-struct ShoulderButtonsViewLeft: View {
- @State var width: CGFloat = 160
- @State var height: CGFloat = 20
- @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
-
- var body: some View {
- HStack {
- ButtonView(button: .leftTrigger)
- .padding(.horizontal)
- ButtonView(button: .leftShoulder)
- .padding(.horizontal)
- }
- .frame(width: width, height: height)
- .onAppear() {
- if UIDevice.current.systemName.contains("iPadOS") {
- width *= 1.2
- height *= 1.2
- }
-
- width *= CGFloat(controllerScale)
- height *= CGFloat(controllerScale)
- }
- }
-}
-
-struct ShoulderButtonsViewRight: View {
- @State var width: CGFloat = 160
- @State var height: CGFloat = 20
- @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
-
- var body: some View {
- HStack {
- ButtonView(button: .rightShoulder)
- .padding(.horizontal)
- ButtonView(button: .rightTrigger)
- .padding(.horizontal)
- }
- .frame(width: width, height: height)
- .onAppear() {
- if UIDevice.current.systemName.contains("iPadOS") {
- width *= 1.2
- height *= 1.2
- }
-
- width *= CGFloat(controllerScale)
- height *= CGFloat(controllerScale)
- }
- }
-}
-
-struct DPadView: View {
- @State var size: CGFloat = 145
- @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
- var body: some View {
- VStack {
- ButtonView(button: .dPadUp)
- HStack {
- ButtonView(button: .dPadLeft)
- Spacer(minLength: 20)
- ButtonView(button: .dPadRight)
- }
- ButtonView(button: .dPadDown)
- .padding(.horizontal)
- }
- .frame(width: size, height: size)
- .onAppear() {
- if UIDevice.current.systemName.contains("iPadOS") {
- size *= 1.2
- }
-
- size *= CGFloat(controllerScale)
- }
- }
-}
-
-struct ABXYView: View {
- @State var size: CGFloat = 145
- @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
-
- var body: some View {
- VStack {
- ButtonView(button: .X)
- HStack {
- ButtonView(button: .Y)
- Spacer(minLength: 20)
- ButtonView(button: .A)
- }
- ButtonView(button: .B)
- .padding(.horizontal)
- }
- .frame(width: size, height: size)
- .onAppear() {
- if UIDevice.current.systemName.contains("iPadOS") {
- size *= 1.2
- }
-
- size *= CGFloat(controllerScale)
- }
- }
-}
-
-struct ButtonView: View {
- var button: VirtualControllerButton
- @State var width: CGFloat = 45
- @State var height: CGFloat = 45
- @State var isPressed = false
- @AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
- @Environment(\.colorScheme) var colorScheme
- @Environment(\.presentationMode) var presentationMode
- @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
-
-
-
- var body: some View {
- Image(systemName: buttonText)
- .resizable()
- .frame(width: width, height: height)
- .foregroundColor(colorScheme == .dark ? Color.gray : Color.gray)
- .opacity(isPressed ? 0.4 : 0.7)
- .gesture(
- DragGesture(minimumDistance: 0)
- .onChanged { _ in
- if !self.isPressed {
- self.isPressed = true
- Ryujinx.shared.virtualController.setButtonState(1, for: button)
- Haptics.shared.play(.heavy)
- }
- }
- .onEnded { _ in
- self.isPressed = false
- Ryujinx.shared.virtualController.setButtonState(0, for: button)
- }
- )
- .onAppear() {
- if button == .leftTrigger || button == .rightTrigger || button == .leftShoulder || button == .rightShoulder {
- width = 65
- }
-
-
- if button == .back || button == .start || button == .guide {
- width = 35
- height = 35
- }
-
- if UIDevice.current.systemName.contains("iPadOS") {
- width *= 1.2
- height *= 1.2
- }
-
- width *= CGFloat(controllerScale)
- height *= CGFloat(controllerScale)
- }
- }
-
-
-
- private var buttonText: String {
- switch button {
- case .A:
- return "a.circle.fill"
- case .B:
- return "b.circle.fill"
- case .X:
- return "x.circle.fill"
- case .Y:
- return "y.circle.fill"
- case .dPadUp:
- return "arrowtriangle.up.circle.fill"
- case .dPadDown:
- return "arrowtriangle.down.circle.fill"
- case .dPadLeft:
- return "arrowtriangle.left.circle.fill"
- case .dPadRight:
- return "arrowtriangle.right.circle.fill"
- case .leftTrigger:
- return"zl.rectangle.roundedtop.fill"
- case .rightTrigger:
- return "zr.rectangle.roundedtop.fill"
- case .leftShoulder:
- return "l.rectangle.roundedbottom.fill"
- case .rightShoulder:
- return "r.rectangle.roundedbottom.fill"
- case .start:
- return "plus.circle.fill" // System symbol for +
- case .back:
- return "minus.circle.fill" // System symbol for -
- case .guide:
- return "house.circle.fill"
- // This should be all the cases
- default:
- return ""
- }
- }
-}
-
-
diff --git a/src/MeloNX/MeloNX/App/Views/ControllerView/Joystick/JoystickView.swift b/src/MeloNX/MeloNX/App/Views/ControllerView/Joystick/JoystickView.swift
deleted file mode 100644
index 7747719c2..000000000
--- a/src/MeloNX/MeloNX/App/Views/ControllerView/Joystick/JoystickView.swift
+++ /dev/null
@@ -1,56 +0,0 @@
-//
-// JoystickView.swift
-// Pomelo
-//
-// Created by Stossy11 on 30/9/2024.
-// Copyright © 2024 Stossy11. All rights reserved.
-//
-
-import SwiftUI
-import SwiftUIJoystick
-
-public struct Joystick: View {
- @State var iscool: Bool? = nil
-
- @ObservedObject public var joystickMonitor = JoystickMonitor()
- @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
- var dragDiameter: CGFloat {
- var selfs = CGFloat(160)
- selfs *= controllerScale
- if UIDevice.current.systemName.contains("iPadOS") {
- return selfs * 1.2
- }
-
- return selfs
- }
- private let shape: JoystickShape = .circle
-
- public var body: some View {
- VStack{
- JoystickBuilder(
- monitor: self.joystickMonitor,
- width: self.dragDiameter,
- shape: .circle,
- background: {
- Text("")
- .hidden()
- },
- foreground: {
- Circle().fill(Color.gray)
- .opacity(0.7)
- },
- locksInPlace: false)
- .onChange(of: self.joystickMonitor.xyPoint) { newValue in
- let scaledX = Float(newValue.x)
- let scaledY = Float(newValue.y) // my dumbass broke this by having -y instead of y :/
- print("Joystick Position: (\(scaledX), \(scaledY))")
-
- if iscool != nil {
- Ryujinx.shared.virtualController.thumbstickMoved(.right, x: newValue.x, y: newValue.y)
- } else {
- Ryujinx.shared.virtualController.thumbstickMoved(.left, x: newValue.x, y: newValue.y)
- }
- }
- }
- }
-}
diff --git a/src/MeloNX/MeloNX/App/Views/Emulation/EmulationView/EmulationView.swift b/src/MeloNX/MeloNX/App/Views/Emulation/EmulationView/EmulationView.swift
deleted file mode 100644
index ac577a973..000000000
--- a/src/MeloNX/MeloNX/App/Views/Emulation/EmulationView/EmulationView.swift
+++ /dev/null
@@ -1,70 +0,0 @@
-//
-// EmulationView.swift
-// MeloNX
-//
-// Created by Stossy11 on 09/02/2025.
-//
-
-import SwiftUI
-
-// Emulation View
-struct EmulationView: View {
- @AppStorage("isVirtualController") var isVCA: Bool = true
- @AppStorage("showScreenShotButton") var ssb: Bool = false
- @State var isPresentedThree: Bool = false
- @State var isAirplaying = Air.shared.connected
- @Environment(\.scenePhase) var scenePhase
- var body: some View {
- ZStack {
- if isAirplaying {
- Text("")
- .onAppear {
- Air.play(AnyView(MetalView().ignoresSafeArea()))
- }
- } else {
- MetalView() // The Emulation View
- .ignoresSafeArea()
- .edgesIgnoringSafeArea(.all)
- }
-
- // Above Emulation View
-
- if isVCA {
- ControllerView() // Virtual Controller
- }
-
-
- if ssb {
- Group {
- VStack {
- Spacer()
-
- HStack {
-
- Button {
- if let screenshot = Ryujinx.shared.emulationUIView.screenshot() {
- UIImageWriteToSavedPhotosAlbum(screenshot, nil, nil, nil)
- }
- } label: {
- Image(systemName: "square.and.arrow.up")
- }
- .frame(width: UIDevice.current.systemName.contains("iPadOS") ? 60 * 1.2 : 45, height: UIDevice.current.systemName.contains("iPadOS") ? 60 * 1.2 : 45)
- .padding()
-
- Spacer()
- }
-
- }
- }
- }
- }
- .onAppear {
- Air.shared.connectionCallbacks.append { cool in
- DispatchQueue.main.async {
- isAirplaying = cool
- print(cool)
- }
- }
- }
- }
-}
diff --git a/src/MeloNX/MeloNX/App/Views/Emulation/MetalView/MetalView.swift b/src/MeloNX/MeloNX/App/Views/Emulation/MetalView/MetalView.swift
deleted file mode 100644
index 59fcb1a70..000000000
--- a/src/MeloNX/MeloNX/App/Views/Emulation/MetalView/MetalView.swift
+++ /dev/null
@@ -1,39 +0,0 @@
-//
-// MetalView.swift
-// MeloNX
-//
-// Created by Stossy11 on 09/02/2025.
-//
-
-import SwiftUI
-import MetalKit
-
-struct MetalView: UIViewRepresentable {
-
- var airplay: Bool = Air.shared.connected // just in case :3
-
- func makeUIView(context: Context) -> UIView {
- let metalLayer = Ryujinx.shared.metalLayer!
-
- var view = UIView()
-
- metalLayer.frame = view.bounds
- if airplay {
- metalLayer.contentsScale = view.contentScaleFactor
- } else {
- Ryujinx.shared.emulationUIView.contentScaleFactor = metalLayer.contentsScale // Right size and Fix Touch :3
- }
-
- Ryujinx.shared.emulationUIView = view
-
- if !Ryujinx.shared.emulationUIView.subviews.contains(where: { $0 == metalLayer }) {
- Ryujinx.shared.emulationUIView.layer.addSublayer(metalLayer)
- }
-
- return Ryujinx.shared.emulationUIView
- }
-
- func updateUIView(_ uiView: UIView, context: Context) {
- // nothin
- }
-}
diff --git a/src/MeloNX/MeloNX/App/Views/Extensions/AppCodableStorage.swift b/src/MeloNX/MeloNX/App/Views/Extensions/AppCodableStorage.swift
new file mode 100644
index 000000000..a85f19f94
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Extensions/AppCodableStorage.swift
@@ -0,0 +1,47 @@
+//
+// AppCodableStorage.swift
+// MeloNX
+//
+// Created by Stossy11 on 12/04/2025.
+//
+
+import SwiftUI
+
+@propertyWrapper
+struct AppCodableStorage: DynamicProperty {
+ @State private var value: Value
+
+ private let key: String
+ private let defaultValue: Value
+ private let storage: UserDefaults
+
+ init(wrappedValue defaultValue: Value, _ key: String, store: UserDefaults = .standard) {
+ self._value = State(initialValue: {
+ if let data = store.data(forKey: key),
+ let decoded = try? JSONDecoder().decode(Value.self, from: data) {
+ return decoded
+ }
+ return defaultValue
+ }())
+ self.key = key
+ self.defaultValue = defaultValue
+ self.storage = store
+ }
+
+ var wrappedValue: Value {
+ get { value }
+ nonmutating set {
+ value = newValue
+ if let data = try? JSONEncoder().encode(newValue) {
+ storage.set(data, forKey: key)
+ }
+ }
+ }
+
+ var projectedValue: Binding {
+ Binding(
+ get: { self.wrappedValue },
+ set: { newValue in self.wrappedValue = newValue }
+ )
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift b/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift
deleted file mode 100644
index 5673ab7ea..000000000
--- a/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift
+++ /dev/null
@@ -1,499 +0,0 @@
-//
-// GameListView.swift
-// MeloNX
-//
-// Created by Stossy11 on 3/11/2024.
-//
-
-import SwiftUI
-import UniformTypeIdentifiers
-
-extension UTType {
- static let nsp = UTType(exportedAs: "com.nintendo.switch-package")
- static let xci = UTType(exportedAs: "com.nintendo.switch-cartridge")
-}
-
-struct GameLibraryView: View {
- @Binding var startemu: Game?
- // @State var importDLCs = false
- @State private var searchText = ""
- @State private var isSearching = false
- @AppStorage("recentGames") private var recentGamesData: Data = Data()
- @State private var recentGames: [Game] = []
- @Environment(\.colorScheme) var colorScheme
- @State var firmwareInstaller = false
- @State var firmwareversion = "0"
- @State var isImporting: Bool = false
- @State var startgame = false
- @State var isSelectingGameFile = false
- @State var isViewingGameInfo: Bool = false
- @State var isSelectingGameUpdate: Bool = false
- @State var isSelectingGameDLC: Bool = false
- @State var gameInfo: Game?
- var games: Binding<[Game]> {
- Binding(
- get: { Ryujinx.shared.games },
- set: { Ryujinx.shared.games = $0 }
- )
- }
-
- var filteredGames: [Game] {
- if searchText.isEmpty {
- return Ryujinx.shared.games.filter { game in
- !realRecentGames.contains(where: { $0.fileURL == game.fileURL })
- }
- }
- return Ryujinx.shared.games.filter {
- $0.titleName.localizedCaseInsensitiveContains(searchText) ||
- $0.developer.localizedCaseInsensitiveContains(searchText)
- }
- }
-
- var realRecentGames: [Game] {
- let games = Ryujinx.shared.games
- return recentGames.compactMap { recentGame in
- games.first(where: { $0.fileURL == recentGame.fileURL })
- }
- }
-
- var body: some View {
- iOSNav {
- List {
- if Ryujinx.shared.games.isEmpty {
- VStack(spacing: 16) {
- Image(systemName: "gamecontroller.fill")
- .font(.system(size: 64))
- .foregroundColor(.secondary.opacity(0.7))
- .padding(.top, 60)
- Text("No Games Found")
- .font(.title2.bold())
- .foregroundColor(.primary)
- Text("Add ROM, Keys and Firmware to get started")
- .font(.subheadline)
- .foregroundColor(.secondary)
- }
- .frame(maxWidth: .infinity)
- .padding(.top, 40)
- } else {
- if !isSearching && !realRecentGames.isEmpty {
- Section {
- ForEach(realRecentGames) { game in
- GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
- .swipeActions(edge: .trailing, allowsFullSwipe: true) {
- Button(role: .destructive) {
- removeFromRecentGames(game)
- } label: {
- Label("Delete", systemImage: "trash")
- }
- }
- }
- } header: {
- Text("Recent")
- }
-
- Section {
- ForEach(filteredGames) { game in
- GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
- }
- } header: {
- Text("Others")
- }
- } else {
- ForEach(filteredGames) { game in
- GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
- }
- }
- }
- }
- .navigationTitle("Games")
- .navigationBarTitleDisplayMode(.large)
- .onAppear {
- loadRecentGames()
-
- let firmware = Ryujinx.shared.fetchFirmwareVersion()
- firmwareversion = (firmware == "" ? "0" : firmware)
- }
- .fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in
- switch result {
- case .success(let url):
- do {
- let fun = url.startAccessingSecurityScopedResource()
- let path = url.path
-
- Ryujinx.shared.installFirmware(firmwarePath: path)
-
- firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion())
- if fun {
- url.stopAccessingSecurityScopedResource()
- }
- }
- case .failure(let error):
- print(error)
- }
- }
- .toolbar {
- ToolbarItem(placement: .topBarTrailing) {
- Button {
- isSelectingGameFile.toggle()
- } label: {
- Image(systemName: "plus")
- }
- }
-
- ToolbarItem(placement: .topBarLeading) {
- Menu {
- Text("Firmware Version: \(firmwareversion)")
- .tint(.white)
-
- if firmwareversion == "0" {
- Button {
- DispatchQueue.main.async {
- firmwareInstaller.toggle()
- }
- } label: {
- Text("Install Firmware")
- }
-
- } else {
- Menu("Firmware") {
- Button {
- Ryujinx.shared.removeFirmware()
- let firmware = Ryujinx.shared.fetchFirmwareVersion()
- firmwareversion = (firmware == "" ? "0" : firmware)
- } label: {
- Text("Remove Firmware")
- }
-
- Button {
- let game = Game(containerFolder: URL(string: "none")!, fileType: .item, fileURL: URL(string: "MiiMaker")!, titleName: "Mii Maker", titleId: "0", developer: "Nintendo", version: firmwareversion)
-
- self.startemu = game
- } label: {
- Text("Mii Maker")
- }
- Button {
- DispatchQueue.main.async {
- isImporting.toggle()
- }
- } label: {
- Text("Open game from system")
- }
- }
- }
-
- Button {
- let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
- var sharedurl = documentsUrl.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://")
- if ProcessInfo.processInfo.isiOSAppOnMac {
- sharedurl = documentsUrl.absoluteString
- }
- print(sharedurl)
- let furl = URL(string: sharedurl)!
- if UIApplication.shared.canOpenURL(furl) {
- UIApplication.shared.open(furl, options: [:])
- }
- } label: {
- Text("Show MeloNX Folder")
- }
- } label: {
- Image(systemName: "ellipsis.circle")
- .foregroundColor(.blue)
- }
- }
- }
- .onChange(of: startemu) { game in
- guard let game else { return }
- addToRecentGames(game)
- }
- }
- .searchable(text: $searchText)
- .animation(.easeInOut, value: searchText)
- .onChange(of: searchText) { _ in
- isSearching = !searchText.isEmpty
- }
- .fileImporter(isPresented: $isImporting, allowedContentTypes: [.zip, .folder, .nsp, .xci]) { result in
- switch result {
- case .success(let url):
- guard url.startAccessingSecurityScopedResource() else {
- print("Failed to access security-scoped resource")
- return
- }
- defer { url.stopAccessingSecurityScopedResource() }
-
- do {
- let handle = try FileHandle(forReadingFrom: url)
- let fileExtension = (url.pathExtension as NSString).utf8String
- let extensionPtr = UnsafeMutablePointer(mutating: fileExtension)
-
- var gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
-
- let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: url)
-
- DispatchQueue.main.async {
- startemu = game
- }
- } catch {
- print(error)
- }
-
- case .failure(let err):
- print("File import failed: \(err.localizedDescription)")
- }
- }
- .fileImporter(isPresented: $isSelectingGameFile, allowedContentTypes: [.nsp, .xci, .zip, .folder]) { result in
- switch result {
- case .success(let url):
- guard url.startAccessingSecurityScopedResource() else {
- print("Failed to access security-scoped resource")
- return
- }
- defer { url.stopAccessingSecurityScopedResource() }
-
- do {
- let fileManager = FileManager.default
- let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
- let romsDirectory = documentsDirectory.appendingPathComponent("roms")
-
- if !fileManager.fileExists(atPath: romsDirectory.path) {
- try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
- }
-
- let destinationURL = romsDirectory.appendingPathComponent(url.lastPathComponent)
- try fileManager.copyItem(at: url, to: destinationURL)
-
- Ryujinx.shared.games = Ryujinx.shared.loadGames()
- } catch {
- print("Error copying game file: \(error)")
- }
- case .failure(let err):
- print("File import failed: \(err.localizedDescription)")
- }
- }
- .sheet(isPresented: $isSelectingGameUpdate) {
- UpdateManagerSheet(game: $gameInfo)
- }
- .sheet(isPresented: $isSelectingGameDLC) {
- DLCManagerSheet(game: $gameInfo)
- }
- .sheet(isPresented: Binding(
- get: { isViewingGameInfo && gameInfo != nil },
- set: { newValue in
- if !newValue {
- isViewingGameInfo = false
- gameInfo = nil
- }
- }
- )) {
- if let game = gameInfo {
- GameInfoSheet(game: game)
- }
- }
- }
-
- private func addToRecentGames(_ game: Game) {
- recentGames.removeAll { $0.titleId == game.titleId }
-
- recentGames.insert(game, at: 0)
-
- if recentGames.count > 5 {
- recentGames = Array(recentGames.prefix(5))
- }
-
- saveRecentGames()
- }
-
- private func removeFromRecentGames(_ game: Game) {
- recentGames.removeAll { $0.titleId == game.titleId }
- saveRecentGames()
- }
-
- private func saveRecentGames() {
- do {
- let encoder = JSONEncoder()
- let data = try encoder.encode(recentGames)
- recentGamesData = data
- } catch {
- print("Error saving recent games: \(error)")
- }
- }
-
- private func loadRecentGames() {
- do {
- let decoder = JSONDecoder()
- recentGames = try decoder.decode([Game].self, from: recentGamesData)
- } catch {
- print("Error loading recent games: \(error)")
- recentGames = []
- }
- }
-
- // MARK: - Delete Game Function
- func deleteGame(game: Game) {
- let fileManager = FileManager.default
- do {
- try fileManager.removeItem(at: game.fileURL)
- Ryujinx.shared.games.removeAll { $0.id == game.id }
- Ryujinx.shared.games = Ryujinx.shared.loadGames()
- } catch {
- print("Error deleting game: \(error)")
- }
- }
-}
-
-// MARK: - Game Model
-extension Game: Codable {
- enum CodingKeys: String, CodingKey {
- case titleName, titleId, developer, version, fileURL
- }
-
- public init(from decoder: Decoder) throws {
- let container = try decoder.container(keyedBy: CodingKeys.self)
- titleName = try container.decode(String.self, forKey: .titleName)
- titleId = try container.decode(String.self, forKey: .titleId)
- developer = try container.decode(String.self, forKey: .developer)
- version = try container.decode(String.self, forKey: .version)
- fileURL = try container.decode(URL.self, forKey: .fileURL)
-
- // Initialize other properties
- self.containerFolder = fileURL.deletingLastPathComponent()
- self.fileType = .item
- }
-
- public func encode(to encoder: Encoder) throws {
- var container = encoder.container(keyedBy: CodingKeys.self)
- try container.encode(titleName, forKey: .titleName)
- try container.encode(titleId, forKey: .titleId)
- try container.encode(developer, forKey: .developer)
- try container.encode(version, forKey: .version)
- try container.encode(fileURL, forKey: .fileURL)
- }
-}
-
-// MARK: - Game List Item
-struct GameListRow: View {
- let game: Game
- @Binding var startemu: Game?
- @Binding var games: [Game] // Add this binding
- @Binding var isViewingGameInfo: Bool
- @Binding var isSelectingGameUpdate: Bool
- @Binding var isSelectingGameDLC: Bool
- @Binding var gameInfo: Game?
- @State var gametoDelete: Game?
- @State var showGameDeleteConfirmation: Bool = false
- @Environment(\.colorScheme) var colorScheme
-
- @AppStorage("portal") var gamepo = false
-
- var body: some View {
- Button(action: {
- startemu = game
- }) {
- HStack(spacing: 16) {
- // Game Icon
- if let icon = game.icon {
- Image(uiImage: icon)
- .resizable()
- .aspectRatio(contentMode: .fill)
- .frame(width: 45, height: 45)
- .cornerRadius(8)
- } else {
- ZStack {
- RoundedRectangle(cornerRadius: 8)
- .fill(colorScheme == .dark ?
- Color(.systemGray5) : Color(.systemGray6))
- .frame(width: 45, height: 45)
-
- Image(systemName: "gamecontroller.fill")
- .font(.system(size: 20))
- .foregroundColor(.gray)
- }
- }
-
- // Game Info
- VStack(alignment: .leading, spacing: 2) {
- Text(game.titleName)
- .font(.body)
- .foregroundColor(.primary)
-
- Text(game.developer)
- .font(.subheadline)
- .foregroundColor(.secondary)
- }
-
- Spacer()
-
- Image(systemName: "play.circle.fill")
- .font(.title2)
- .foregroundColor(.accentColor)
- .opacity(0.8)
- }
- }
- .contextMenu {
- Section {
- Button {
- startemu = game
- } label: {
- Label("Play Now", systemImage: "play.fill")
- }
-
- Button {
- gameInfo = game
- isViewingGameInfo.toggle()
-
- if game.titleName.lowercased() == "portal" {
- gamepo = true
- } else if game.titleName.lowercased() == "portal 2" {
- gamepo = true
- }
- } label: {
- Label("Game Info", systemImage: "info.circle")
- }
- }
-
- Section {
- Button {
- gameInfo = game
- isSelectingGameUpdate.toggle()
- } label: {
- Label("Game Update Manager", systemImage: "chevron.up.circle")
- }
-
- Button {
- gameInfo = game
- isSelectingGameDLC.toggle()
- } label: {
- Label("Game DLC Manager", systemImage: "plus.viewfinder")
- }
- }
-
- Section {
- Button(role: .destructive) {
- gametoDelete = game
- showGameDeleteConfirmation.toggle()
- } label: {
- Label("Delete", systemImage: "trash")
- }
- }
- }
- .confirmationDialog("Are you sure you want to delete this game?", isPresented: $showGameDeleteConfirmation) {
- Button("Delete", role: .destructive) {
- if let game = gametoDelete {
- deleteGame(game: game)
- }
- }
- Button("Cancel", role: .cancel) {}
- } message: {
- Text("Are you sure you want to delete \(gametoDelete?.titleName ?? "this game")?")
- }
- }
-
- private func deleteGame(game: Game) {
- let fileManager = FileManager.default
- do {
- try fileManager.removeItem(at: game.fileURL)
- games.removeAll { $0.id == game.id }
- } catch {
- print("Error deleting game: \(error)")
- }
- }
-}
diff --git a/src/MeloNX/MeloNX/App/Views/Logging/Logs.swift b/src/MeloNX/MeloNX/App/Views/Logging/Logs.swift
deleted file mode 100644
index 108e3b796..000000000
--- a/src/MeloNX/MeloNX/App/Views/Logging/Logs.swift
+++ /dev/null
@@ -1,100 +0,0 @@
-//
-// LogEntry.swift
-// MeloNX
-//
-// Created by Stossy11 on 09/02/2025.
-//
-
-
-import SwiftUI
-
-struct LogEntry: Identifiable, Equatable {
- let id = UUID()
- let text: String
-
- static func == (lhs: LogEntry, rhs: LogEntry) -> Bool {
- return lhs.id == rhs.id && lhs.text == rhs.text
- }
-}
-
-struct LogViewer: View {
- @State private var logs: [LogEntry] = []
- @State private var latestLogFilePath: String?
-
- var body: some View {
- VStack {
- Spacer()
- VStack {
- ForEach(logs) { log in
- Text(log.text)
- .padding(4)
- .background(Color.black.opacity(0.7))
- .foregroundColor(.white)
- .cornerRadius(8)
- .transition(.move(edge: .top).combined(with: .opacity))
- .animation(.easeOut(duration: 2), value: logs)
- }
- }
- .frame(maxWidth: .infinity)
- .padding()
- }
- .edgesIgnoringSafeArea(.all)
- .onAppear {
- findNewestLogFile()
- }
- }
-
- func findNewestLogFile() {
- let logsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("logs")
-
- guard let directory = logsDirectory else { return }
-
- do {
- let logFiles = try FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: [.contentModificationDateKey], options: .skipsHiddenFiles)
-
- // Sort files by modification date (newest first)
- let sortedFiles = logFiles.sorted {
- (try? $0.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? Date.distantPast >
- (try? $1.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? Date.distantPast
- }
-
- if let newestLogFile = sortedFiles.first {
- latestLogFilePath = newestLogFile.path
- startReadingLogFile()
- }
- } catch {
- print("Error reading log files: \(error)")
- }
- }
-
- func startReadingLogFile() {
- guard let path = latestLogFilePath else { return }
- let fileHandle = try? FileHandle(forReadingAtPath: path)
- fileHandle?.seekToEndOfFile()
-
- NotificationCenter.default.addObserver(forName: .NSFileHandleDataAvailable, object: fileHandle, queue: .main) { _ in
- if let data = fileHandle?.availableData, !data.isEmpty {
- if let logLine = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) {
- DispatchQueue.main.async {
- withAnimation {
- logs.append(LogEntry(text: logLine))
- }
- // Remove old logs after a delay
- DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
- withAnimation {
- removelogfirst()
- }
- }
- }
- }
- }
- fileHandle?.waitForDataInBackgroundAndNotify()
- }
-
- fileHandle?.waitForDataInBackgroundAndNotify()
- }
-
- func removelogfirst() {
- logs.removeFirst()
- }
-}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Elements/FileImporter.swift b/src/MeloNX/MeloNX/App/Views/Main/Elements/FileImporter.swift
new file mode 100644
index 000000000..bfae480a2
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/Elements/FileImporter.swift
@@ -0,0 +1,125 @@
+//
+// FileImporter.swift
+// MeloNX
+//
+// Created by Stossy11 on 17/04/2025.
+//
+
+
+import SwiftUI
+import UniformTypeIdentifiers
+
+class FileImporterManager: ObservableObject {
+ static let shared = FileImporterManager()
+
+ private init() {}
+
+ func importFiles(types: [UTType], allowMultiple: Bool = false, completion: @escaping (Result<[URL], Error>) -> Void) {
+ let id = "\(Unmanaged.passUnretained(completion as AnyObject).toOpaque())"
+
+ DispatchQueue.main.async {
+ NotificationCenter.default.post(
+ name: .importFiles,
+ object: nil,
+ userInfo: [
+ "id": id,
+ "types": types,
+ "allowMultiple": allowMultiple,
+ "completion": completion
+ ]
+ )
+ }
+ }
+}
+
+extension Notification.Name {
+ static let importFiles = Notification.Name("importFiles")
+}
+
+struct FileImporterView: ViewModifier {
+ @State private var isImporterPresented: [String: Bool] = [:]
+ @State private var activeImporters: [String: ImporterConfig] = [:]
+
+ struct ImporterConfig {
+ let types: [UTType]
+ let allowMultiple: Bool
+ let completion: (Result<[URL], Error>) -> Void
+ }
+
+ func body(content: Content) -> some View {
+ content
+ .background(
+ ForEach(Array(activeImporters.keys), id: \.self) { id in
+ if let config = activeImporters[id] {
+ FileImporterWrapper(
+ isPresented: Binding(
+ get: { isImporterPresented[id] ?? false },
+ set: { isImporterPresented[id] = $0 }
+ ),
+ id: id,
+ config: config,
+ onCompletion: { success in
+ if success {
+ DispatchQueue.main.async {
+ activeImporters.removeValue(forKey: id)
+ }
+ }
+ }
+ )
+ }
+ }
+ )
+ .onReceive(NotificationCenter.default.publisher(for: .importFiles)) { notification in
+ guard let userInfo = notification.userInfo,
+ let id = userInfo["id"] as? String,
+ let types = userInfo["types"] as? [UTType],
+ let allowMultiple = userInfo["allowMultiple"] as? Bool,
+ let completion = userInfo["completion"] as? ((Result<[URL], Error>) -> Void) else {
+ return
+ }
+
+ let config = ImporterConfig(
+ types: types,
+ allowMultiple: allowMultiple,
+ completion: completion
+ )
+
+ activeImporters[id] = config
+
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ isImporterPresented[id] = true
+ }
+ }
+ }
+}
+
+struct FileImporterWrapper: View {
+ @Binding var isPresented: Bool
+ let id: String
+ let config: FileImporterView.ImporterConfig
+ let onCompletion: (Bool) -> Void
+
+ var body: some View {
+ Text("wow")
+ .hidden()
+ .fileImporter(
+ isPresented: $isPresented,
+ allowedContentTypes: config.types,
+ allowsMultipleSelection: config.allowMultiple
+ ) { result in
+ switch result {
+ case .success(let urls):
+ config.completion(.success(urls))
+ case .failure(let error):
+ config.completion(.failure(error))
+ }
+ onCompletion(true)
+ }
+ }
+}
+
+extension View {
+ func withFileImporter() -> some View {
+ self.modifier(FileImporterView())
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Emulation/AirPlay/Air.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/Air.swift
similarity index 89%
rename from src/MeloNX/MeloNX/App/Views/Emulation/AirPlay/Air.swift
rename to src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/Air.swift
index 8842c50ea..4230ffae1 100644
--- a/src/MeloNX/MeloNX/App/Views/Emulation/AirPlay/Air.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/Air.swift
@@ -58,7 +58,7 @@ public class Air {
}
@objc func didConnect(sender: NSNotification) {
- print("AirKit - Connect")
+ // print("AirKit - Connect")
self.connected = true
guard let screen: UIScreen = sender.object as? UIScreen else { return }
add(screen: screen) { success in
@@ -69,35 +69,35 @@ public class Air {
func add(screen: UIScreen, completion: @escaping (Bool) -> ()) {
- print("AirKit - Add Screen")
+ // print("AirKit - Add Screen")
airScreen = screen
airWindow = UIWindow(frame: airScreen!.bounds)
guard let viewController: UIViewController = hostingController else {
- print("AirKit - Add - Failed: Hosting Controller Not Found")
+ // print("AirKit - Add - Failed: Hosting Controller Not Found")
completion(false)
return
}
findWindowScene(for: airScreen!) { windowScene in
guard let airWindowScene: UIWindowScene = windowScene else {
- print("AirKit - Add - Failed: Window Scene Not Found")
+ // print("AirKit - Add - Failed: Window Scene Not Found")
completion(false)
return
}
self.airWindow?.rootViewController = viewController
self.airWindow?.windowScene = airWindowScene
self.airWindow?.isHidden = false
- print("AirKit - Add Screen - Done")
+ // print("AirKit - Add Screen - Done")
completion(true)
}
}
func findWindowScene(for screen: UIScreen, shouldRecurse: Bool = true, completion: @escaping (UIWindowScene?) -> ()) {
- print("AirKit - Find Window Scene")
+ // print("AirKit - Find Window Scene")
var matchingWindowScene: UIWindowScene? = nil
let scenes = UIApplication.shared.connectedScenes
for scene in scenes {
@@ -120,23 +120,23 @@ public class Air {
}
@objc func didDisconnect() {
- print("AirKit - Disconnect")
+ // print("AirKit - Disconnect")
remove()
connected = false
}
func remove() {
- print("AirKit - Remove")
+ // print("AirKit - Remove")
airWindow = nil
airScreen = nil
}
@objc func didBecomeActive() {
- print("AirKit - App Active")
+ // print("AirKit - App Active")
}
@objc func willResignActive() {
- print("AirKit - App Inactive")
+ // print("AirKit - App Inactive")
}
diff --git a/src/MeloNX/MeloNX/App/Views/Emulation/AirPlay/AirPlay.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/AirPlay.swift
similarity index 81%
rename from src/MeloNX/MeloNX/App/Views/Emulation/AirPlay/AirPlay.swift
rename to src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/AirPlay.swift
index a3c90b241..0eeb7c835 100644
--- a/src/MeloNX/MeloNX/App/Views/Emulation/AirPlay/AirPlay.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/AirPlay.swift
@@ -4,7 +4,7 @@ import SwiftUI
public extension View {
func airPlay() -> some View {
- print("AirKit - airPlay")
+ // print("AirKit - airPlay")
Air.play(AnyView(self))
return self
}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/ControllerView.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/ControllerView.swift
new file mode 100644
index 000000000..aaf5f758a
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/ControllerView.swift
@@ -0,0 +1,558 @@
+//
+// ControllerView.swift
+// Pomelo-V2
+//
+// Created by Stossy11 on 16/7/2024.
+//
+
+import SwiftUI
+import GameController
+import CoreMotion
+
+struct ControllerView: View {
+ // MARK: - Properties
+ @AppStorage("On-ScreenControllerScale") private var controllerScale: Double = 1.0
+ @AppStorage("stick-button") private var stickButton = false
+ @State private var isPortrait = true
+ @State var hideDpad = false
+ @State var hideABXY = false
+ @Environment(\.verticalSizeClass) var verticalSizeClass
+
+
+ // MARK: - Body
+ var body: some View {
+ Group {
+ let isPad = UIDevice.current.userInterfaceIdiom == .pad
+
+ if isPortrait && !isPad {
+ portraitLayout
+ } else {
+ landscapeLayout
+ }
+ }
+ .padding()
+ .onChange(of: verticalSizeClass) { _ in
+ updateOrientation()
+ }
+ .onAppear(perform: updateOrientation)
+ }
+
+ // MARK: - Layouts
+ private var portraitLayout: some View {
+ VStack {
+ Spacer()
+ VStack(spacing: 20) {
+ HStack(spacing: 30) {
+ VStack(spacing: 15) {
+ ShoulderButtonsViewLeft()
+ .padding(.vertical)
+ ZStack {
+ JoystickController(showBackground: $hideDpad)
+ DPadView()
+ .opacity(hideDpad ? 0 : 1)
+ .allowsHitTesting(!hideDpad)
+ .animation(.easeInOut(duration: 0.2), value: hideDpad)
+ }
+ }
+
+ VStack(spacing: 15) {
+ ShoulderButtonsViewRight()
+ .padding(.vertical)
+ ZStack {
+ JoystickController(iscool: true, showBackground: $hideABXY)
+ ABXYView()
+ .opacity(hideABXY ? 0 : 1)
+ .allowsHitTesting(!hideABXY)
+ .animation(.easeInOut(duration: 0.2), value: hideABXY)
+ }
+ }
+ }
+
+ HStack(spacing: 60) {
+ HStack {
+ ButtonView(button: .leftStick)
+ .padding()
+ ButtonView(button: .back)
+ }
+
+ HStack {
+ ButtonView(button: .start)
+ ButtonView(button: .rightStick)
+ .padding()
+ }
+ }
+ }
+ }
+ }
+
+ private var landscapeLayout: some View {
+ VStack {
+ Spacer()
+
+ HStack {
+ VStack(spacing: 20) {
+ ShoulderButtonsViewLeft()
+ .padding(.vertical)
+ ZStack {
+ JoystickController(showBackground: $hideDpad)
+ DPadView()
+ .opacity(hideDpad ? 0 : 1)
+ .allowsHitTesting(!hideDpad)
+ .animation(.easeInOut(duration: 0.2), value: hideDpad)
+ }
+ }
+
+ Spacer()
+
+ centerButtons
+
+ Spacer()
+
+ VStack(spacing: 20) {
+ ShoulderButtonsViewRight()
+ .padding(.vertical)
+ ZStack {
+ JoystickController(iscool: true, showBackground: $hideABXY)
+ ABXYView()
+ .opacity(hideABXY ? 0 : 1)
+ .allowsHitTesting(!hideABXY)
+ .animation(.easeInOut(duration: 0.2), value: hideABXY)
+ }
+ }
+ }
+ }
+ }
+
+ private var centerButtons: some View {
+ Group {
+ if stickButton {
+ VStack {
+ HStack(spacing: 50) {
+ ButtonView(button: .leftStick)
+ .padding()
+ Spacer()
+ ButtonView(button: .rightStick)
+ .padding()
+ }
+ .padding(.top, 30)
+
+ HStack(spacing: 50) {
+ ButtonView(button: .back)
+ Spacer()
+ ButtonView(button: .start)
+ }
+ }
+ .padding(.bottom, 20)
+ } else {
+ HStack(spacing: 50) {
+ ButtonView(button: .back)
+ Spacer()
+ ButtonView(button: .start)
+ }
+ .padding(.bottom, 20)
+ }
+ }
+ }
+
+ // MARK: - Methods
+
+ private func updateOrientation() {
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+ let window = windowScene.windows.first {
+ isPortrait = window.bounds.size.height > window.bounds.size.width
+ }
+ }
+}
+
+
+struct ShoulderButtonsViewLeft: View {
+ @State private var width: CGFloat = 160
+ @State private var height: CGFloat = 20
+ @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
+
+ var body: some View {
+ HStack(spacing: 20) {
+ ButtonView(button: .leftTrigger)
+ ButtonView(button: .leftShoulder)
+ }
+ .frame(width: width, height: height)
+ .onAppear {
+ if UIDevice.current.systemName.contains("iPadOS") {
+ width *= 1.2
+ height *= 1.2
+ }
+
+ width *= CGFloat(controllerScale)
+ height *= CGFloat(controllerScale)
+ }
+ }
+}
+
+struct ShoulderButtonsViewRight: View {
+ @State private var width: CGFloat = 160
+ @State private var height: CGFloat = 20
+ @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
+
+ var body: some View {
+ HStack(spacing: 20) {
+ ButtonView(button: .rightShoulder)
+ ButtonView(button: .rightTrigger)
+ }
+ .frame(width: width, height: height)
+ .onAppear {
+ if UIDevice.current.systemName.contains("iPadOS") {
+ width *= 1.2
+ height *= 1.2
+ }
+
+ width *= CGFloat(controllerScale)
+ height *= CGFloat(controllerScale)
+ }
+ }
+}
+
+struct DPadView: View {
+ @State private var size: CGFloat = 145
+ @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
+
+ var body: some View {
+ VStack(spacing: 7) {
+ ButtonView(button: .dPadUp)
+ HStack(spacing: 22) {
+ ButtonView(button: .dPadLeft)
+ Spacer(minLength: 22)
+ ButtonView(button: .dPadRight)
+ }
+ ButtonView(button: .dPadDown)
+ }
+ .frame(width: size, height: size)
+ .onAppear {
+ if UIDevice.current.systemName.contains("iPadOS") {
+ size *= 1.2
+ }
+
+ size *= CGFloat(controllerScale)
+ }
+ }
+}
+
+struct ABXYView: View {
+ @State private var size: CGFloat = 145
+ @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
+
+ var body: some View {
+ VStack(spacing: 7) {
+ ButtonView(button: .X)
+ HStack(spacing: 22) {
+ ButtonView(button: .Y)
+ Spacer(minLength: 22)
+ ButtonView(button: .A)
+ }
+ ButtonView(button: .B)
+ }
+ .frame(width: size, height: size)
+ .onAppear {
+ if UIDevice.current.systemName.contains("iPadOS") {
+ size *= 1.2
+ }
+
+ size *= CGFloat(controllerScale)
+ }
+ }
+}
+
+
+struct ButtonView: View {
+ var button: VirtualControllerButton
+
+ @AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
+ @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
+ @Environment(\.presentationMode) var presentationMode
+
+ @AppCodableStorage("toggleButtons") var toggleButtons = ToggleButtonsState()
+ @State private var istoggle = false
+
+ @State private var isPressed = false
+ @State private var toggleState = false
+
+ @State private var size: CGSize = .zero
+
+ var body: some View {
+ Circle()
+ .foregroundStyle(.clear.opacity(0))
+ .overlay {
+ Image(systemName: buttonConfig.iconName)
+ .resizable()
+ .scaledToFit()
+ .frame(width: size.width, height: size.height)
+ .foregroundStyle(.white)
+ .opacity(isPressed ? 0.6 : 0.8)
+ .allowsHitTesting(false)
+ }
+ .frame(width: size.width, height: size.height)
+ .background(
+ buttonBackground
+ )
+ .gesture(
+ DragGesture(minimumDistance: 0)
+ .onChanged { _ in handleButtonPress() }
+ .onEnded { _ in handleButtonRelease() }
+ )
+ .onAppear {
+ istoggle = (toggleButtons.toggle1 && button == .A) || (toggleButtons.toggle2 && button == .B) || (toggleButtons.toggle3 && button == .X) || (toggleButtons.toggle4 && button == .Y)
+ size = calculateButtonSize()
+ }
+ .onChange(of: controllerScale) { _ in
+ size = calculateButtonSize()
+ }
+ }
+
+ private var buttonBackground: some View {
+ Group {
+ if !button.isTrigger && button != .leftStick && button != .rightStick {
+ Circle()
+ .fill(Color.gray.opacity(0.4))
+ .frame(width: size.width * 1.25, height: size.height * 1.25)
+ } else if button == .leftStick || button == .rightStick {
+ Image(systemName: buttonConfig.iconName)
+ .resizable()
+ .scaledToFit()
+ .frame(width: size.width * 1.25, height: size.height * 1.25)
+ .foregroundColor(Color.gray.opacity(0.4))
+ } else if button.isTrigger {
+ Image(systemName: convertTriggerIconToButton(buttonConfig.iconName))
+ .resizable()
+ .scaledToFit()
+ .frame(width: size.width * 1.25, height: size.height * 1.25)
+ .foregroundColor(Color.gray.opacity(0.4))
+ }
+ }
+ }
+
+ private func convertTriggerIconToButton(_ iconName: String) -> String {
+ if iconName.hasPrefix("zl") || iconName.hasPrefix("zr") {
+ var converted = String(iconName.dropFirst(3))
+ converted = converted.replacingOccurrences(of: "rectangle", with: "button")
+ converted = converted.replacingOccurrences(of: ".fill", with: ".horizontal.fill")
+ return converted
+ } else {
+ var converted = String(iconName.dropFirst(2))
+ converted = converted.replacingOccurrences(of: "rectangle", with: "button")
+ converted = converted.replacingOccurrences(of: ".fill", with: ".horizontal.fill")
+ return converted
+ }
+ }
+
+ private func handleButtonPress() {
+ guard !isPressed || istoggle else { return }
+
+ if istoggle {
+ toggleState.toggle()
+ isPressed = toggleState
+ let value = toggleState ? 1 : 0
+ Ryujinx.shared.virtualController.setButtonState(Uint8(value), for: button)
+ Haptics.shared.play(.medium)
+ } else {
+ isPressed = true
+ Ryujinx.shared.virtualController.setButtonState(1, for: button)
+ Haptics.shared.play(.medium)
+ }
+ }
+
+ private func handleButtonRelease() {
+ if istoggle { return }
+
+ guard isPressed else { return }
+
+ isPressed = false
+ DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.05) {
+ Ryujinx.shared.virtualController.setButtonState(0, for: button)
+ }
+ }
+
+ private func calculateButtonSize() -> CGSize {
+ let baseWidth: CGFloat
+ let baseHeight: CGFloat
+
+ if button.isTrigger {
+ baseWidth = 70
+ baseHeight = 40
+ } else if button.isSmall {
+ baseWidth = 35
+ baseHeight = 35
+ } else {
+ baseWidth = 45
+ baseHeight = 45
+ }
+
+ let deviceMultiplier = UIDevice.current.userInterfaceIdiom == .pad ? 1.2 : 1.0
+ let scaleMultiplier = CGFloat(controllerScale)
+
+ return CGSize(
+ width: baseWidth * deviceMultiplier * scaleMultiplier,
+ height: baseHeight * deviceMultiplier * scaleMultiplier
+ )
+ }
+
+ // Centralized button configuration
+ private var buttonConfig: ButtonConfiguration {
+ switch button {
+ case .A:
+ return ButtonConfiguration(iconName: "a.circle.fill")
+ case .B:
+ return ButtonConfiguration(iconName: "b.circle.fill")
+ case .X:
+ return ButtonConfiguration(iconName: "x.circle.fill")
+ case .Y:
+ return ButtonConfiguration(iconName: "y.circle.fill")
+ case .leftStick:
+ return ButtonConfiguration(iconName: "l.joystick.press.down.fill")
+ case .rightStick:
+ return ButtonConfiguration(iconName: "r.joystick.press.down.fill")
+ case .dPadUp:
+ return ButtonConfiguration(iconName: "arrowtriangle.up.circle.fill")
+ case .dPadDown:
+ return ButtonConfiguration(iconName: "arrowtriangle.down.circle.fill")
+ case .dPadLeft:
+ return ButtonConfiguration(iconName: "arrowtriangle.left.circle.fill")
+ case .dPadRight:
+ return ButtonConfiguration(iconName: "arrowtriangle.right.circle.fill")
+ case .leftTrigger:
+ return ButtonConfiguration(iconName: "zl.rectangle.roundedtop.fill")
+ case .rightTrigger:
+ return ButtonConfiguration(iconName: "zr.rectangle.roundedtop.fill")
+ case .leftShoulder:
+ return ButtonConfiguration(iconName: "l.rectangle.roundedbottom.fill")
+ case .rightShoulder:
+ return ButtonConfiguration(iconName: "r.rectangle.roundedbottom.fill")
+ case .start:
+ return ButtonConfiguration(iconName: "plus.circle.fill")
+ case .back:
+ return ButtonConfiguration(iconName: "minus.circle.fill")
+ case .guide:
+ return ButtonConfiguration(iconName: "house.circle.fill")
+ }
+ }
+
+ struct ButtonConfiguration {
+ let iconName: String
+ }
+}
+
+
+struct ExtButtonIconView: View {
+ var button: VirtualControllerButton
+ var opacity = 0.8
+
+ @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
+ @State private var size: CGSize = .zero
+
+ var body: some View {
+ Circle()
+ .foregroundStyle(.clear.opacity(0))
+ .overlay {
+ Image(systemName: buttonConfig.iconName)
+ .resizable()
+ .scaledToFit()
+ .frame(width: size.width / 1.5, height: size.height / 1.5)
+ .foregroundStyle(.white)
+ .opacity(opacity)
+ .allowsHitTesting(false)
+ }
+ .frame(width: size.width, height: size.height)
+ .background(
+ buttonBackground
+ )
+ .onAppear {
+ size = calculateButtonSize()
+ }
+ .onChange(of: controllerScale) { _ in
+ size = calculateButtonSize()
+ }
+ }
+
+ private var buttonBackground: some View {
+ Group {
+ if !button.isTrigger && button != .leftStick && button != .rightStick {
+ Circle()
+ .fill(Color.gray.opacity(0.3))
+ .frame(width: size.width * 1.25, height: size.height * 1.25)
+ } else if button == .leftStick || button == .rightStick {
+ Image(systemName: buttonConfig.iconName)
+ .resizable()
+ .scaledToFit()
+ .frame(width: size.width * 1.25, height: size.height * 1.25)
+ .foregroundColor(Color.gray.opacity(0.4))
+ } else if button.isTrigger {
+ Image(systemName: convertTriggerIconToButton(buttonConfig.iconName))
+ .resizable()
+ .scaledToFit()
+ .frame(width: size.width * 1.25, height: size.height * 1.25)
+ .foregroundColor(Color.gray.opacity(0.4))
+ }
+ }
+ }
+
+ private func convertTriggerIconToButton(_ iconName: String) -> String {
+ var converted = iconName
+ if iconName.hasPrefix("zl") || iconName.hasPrefix("zr") {
+ converted = String(iconName.dropFirst(3))
+ } else {
+ converted = String(iconName.dropFirst(2))
+ }
+ converted = converted
+ .replacingOccurrences(of: "rectangle", with: "button")
+ .replacingOccurrences(of: ".fill", with: ".horizontal.fill")
+ return converted
+ }
+
+ private func calculateButtonSize() -> CGSize {
+ let baseWidth: CGFloat
+ let baseHeight: CGFloat
+
+ if button.isTrigger {
+ baseWidth = 70
+ baseHeight = 40
+ } else if button.isSmall {
+ baseWidth = 35
+ baseHeight = 35
+ } else {
+ baseWidth = 45
+ baseHeight = 45
+ }
+
+ let deviceMultiplier = UIDevice.current.userInterfaceIdiom == .pad ? 1.2 : 1.0
+ let scaleMultiplier = CGFloat(controllerScale)
+
+ return CGSize(
+ width: baseWidth * deviceMultiplier * scaleMultiplier,
+ height: baseHeight * deviceMultiplier * scaleMultiplier
+ )
+ }
+
+ private var buttonConfig: ButtonConfiguration {
+ switch button {
+ case .A: return .init(iconName: "a.circle.fill")
+ case .B: return .init(iconName: "b.circle.fill")
+ case .X: return .init(iconName: "x.circle.fill")
+ case .Y: return .init(iconName: "y.circle.fill")
+ case .leftStick: return .init(iconName: "l.joystick.press.down.fill")
+ case .rightStick: return .init(iconName: "r.joystick.press.down.fill")
+ case .dPadUp: return .init(iconName: "arrowtriangle.up.circle.fill")
+ case .dPadDown: return .init(iconName: "arrowtriangle.down.circle.fill")
+ case .dPadLeft: return .init(iconName: "arrowtriangle.left.circle.fill")
+ case .dPadRight: return .init(iconName: "arrowtriangle.right.circle.fill")
+ case .leftTrigger: return .init(iconName: "zl.rectangle.roundedtop.fill")
+ case .rightTrigger: return .init(iconName: "zr.rectangle.roundedtop.fill")
+ case .leftShoulder: return .init(iconName: "l.rectangle.roundedbottom.fill")
+ case .rightShoulder: return .init(iconName: "r.rectangle.roundedbottom.fill")
+ case .start: return .init(iconName: "plus.circle.fill")
+ case .back: return .init(iconName: "minus.circle.fill")
+ case .guide: return .init(iconName: "gearshape.fill")
+ }
+ }
+
+ struct ButtonConfiguration {
+ let iconName: String
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/ControllerView/Haptics/Haptics.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/Haptics/Haptics.swift
similarity index 95%
rename from src/MeloNX/MeloNX/App/Views/ControllerView/Haptics/Haptics.swift
rename to src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/Haptics/Haptics.swift
index 5dd555815..4409a4da2 100644
--- a/src/MeloNX/MeloNX/App/Views/ControllerView/Haptics/Haptics.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/Haptics/Haptics.swift
@@ -15,7 +15,6 @@ class Haptics {
private init() { }
func play(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle) {
- print("haptics")
UIImpactFeedbackGenerator(style: feedbackStyle).impactOccurred()
}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/Joystick/Joystick.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/Joystick/Joystick.swift
new file mode 100644
index 000000000..67a7615f5
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/Joystick/Joystick.swift
@@ -0,0 +1,93 @@
+//
+// Joystick.swift
+// MeloNX
+//
+// Created by Stossy11 on 21/03/2025.
+//
+
+
+import SwiftUI
+
+struct Joystick: View {
+ @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
+
+ @Binding var position: CGPoint
+ @State var joystickSize: CGFloat
+ var boundarySize: CGFloat
+
+ @State private var offset: CGSize = .zero
+ @Binding var showBackground: Bool
+
+ let sensitivity: CGFloat = 1.2
+
+
+ var dragGesture: some Gesture {
+ DragGesture()
+ .onChanged { value in
+ withAnimation(.easeIn) {
+ showBackground = true
+ }
+
+ let translation = value.translation
+ let distance = sqrt(translation.width * translation.width + translation.height * translation.height)
+ let maxRadius = (boundarySize - joystickSize) / 2
+ let extendedRadius = maxRadius + (joystickSize / 2)
+
+ if distance <= extendedRadius {
+ offset = translation
+ } else {
+ let angle = atan2(translation.height, translation.width)
+ offset = CGSize(width: cos(angle) * extendedRadius, height: sin(angle) * extendedRadius)
+ }
+
+ position = CGPoint(
+ x: max(-1, min(1, (offset.width / extendedRadius) * sensitivity)),
+ y: max(-1, min(1, (offset.height / extendedRadius) * sensitivity))
+ )
+ }
+ .onEnded { _ in
+ offset = .zero
+ position = .zero
+ withAnimation(.easeOut) {
+ showBackground = false
+ }
+ }
+ }
+
+ var body: some View {
+ ZStack {
+ Circle()
+ .fill(Color.clear.opacity(0))
+ .frame(width: boundarySize, height: boundarySize)
+ .scaleEffect(controllerScale)
+
+ if showBackground {
+ Circle()
+ .fill(Color.gray.opacity(0.4))
+ .frame(width: boundarySize, height: boundarySize)
+ .animation(.easeInOut(duration: 0.1), value: showBackground)
+ .scaleEffect(controllerScale)
+ }
+
+ Circle()
+ .fill(Color.white.opacity(0.5))
+ .frame(width: joystickSize, height: joystickSize)
+ .background(
+ Circle()
+ .fill(Color.gray.opacity(0.3))
+ .frame(width: joystickSize * 1.25, height: joystickSize * 1.25)
+ )
+ .offset(offset)
+ .gesture(dragGesture)
+ .scaleEffect(controllerScale)
+ }
+ .frame(width: boundarySize, height: boundarySize)
+ .onChange(of: showBackground) { newValue in
+ if newValue {
+ joystickSize *= 1.4
+ } else {
+ joystickSize = (boundarySize * 0.2)
+ }
+ }
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/Joystick/JoystickView.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/Joystick/JoystickView.swift
new file mode 100644
index 000000000..2459c6a7c
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/Joystick/JoystickView.swift
@@ -0,0 +1,39 @@
+//
+// JoystickView.swift
+// Pomelo
+//
+// Created by Stossy11 on 30/9/2024.
+// Copyright © 2024 Stossy11. All rights reserved.
+//
+
+import SwiftUI
+
+struct JoystickController: View {
+ @State var iscool: Bool? = nil
+ @Environment(\.colorScheme) var colorScheme
+ @Binding var showBackground: Bool
+ @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
+ @State var position: CGPoint = CGPoint(x: 0, y: 0)
+ var dragDiameter: CGFloat {
+ var selfs = CGFloat(160)
+ // selfs *= controllerScale
+ if UIDevice.current.systemName.contains("iPadOS") {
+ return selfs * 1.2
+ }
+
+ return selfs
+ }
+
+ public var body: some View {
+ VStack {
+ Joystick(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)
+ }
+ }
+ }
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/EmulationView.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/EmulationView.swift
new file mode 100644
index 000000000..58a2b6d49
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/EmulationView.swift
@@ -0,0 +1,175 @@
+//
+// EmulationView.swift
+// MeloNX
+//
+// Created by Stossy11 on 09/02/2025.
+//
+
+import SwiftUI
+
+// Emulation View
+struct EmulationView: View {
+ @AppStorage("performacehud") var performacehud: Bool = false
+ @AppStorage("isVirtualController") var isVCA: Bool = true
+ @AppStorage("showScreenShotButton") var ssb: Bool = false
+ @AppStorage("showlogsgame") var showlogsgame: Bool = false
+
+ @AppStorage("On-ScreenControllerOpacity") var controllerOpacity: Double = 1.0
+
+ @AppStorage("disableTouch") var blackScreen = false
+
+ @State var isPresentedThree: Bool = false
+ @State var isAirplaying = Air.shared.connected
+ @Binding var startgame: Game?
+
+ @Environment(\.scenePhase) var scenePhase
+ @State private var isInBackground = false
+ @State var showSettings = false
+ @State var pauseEmu = true
+ @AppStorage("location-enabled") var locationenabled: Bool = false
+
+ var body: some View {
+ ZStack {
+ if isAirplaying {
+ TouchView()
+ .ignoresSafeArea()
+ .edgesIgnoringSafeArea(.all)
+ .onAppear {
+ Air.play(AnyView(MetalView().ignoresSafeArea().edgesIgnoringSafeArea(.all)))
+ }
+
+ Color.black
+ .ignoresSafeArea()
+ .edgesIgnoringSafeArea(.all)
+ .allowsHitTesting(false)
+ } else {
+ MetalView() // The Emulation View
+ .ignoresSafeArea()
+ .edgesIgnoringSafeArea(.all)
+ }
+
+ // Above Emulation View
+
+ if isVCA {
+ ControllerView() // Virtual Controller
+ .opacity(controllerOpacity)
+ .allowsHitTesting(true)
+ }
+
+ Group {
+ VStack {
+ HStack {
+ if performacehud, !showlogsgame {
+ PerformanceOverlayView()
+ }
+
+ Spacer()
+
+ if performacehud, showlogsgame {
+ PerformanceOverlayView()
+ }
+ }
+
+ HStack {
+ if showlogsgame, get_current_fps() != 0 {
+ LogFileView(isfps: false)
+ }
+
+ Spacer()
+ }
+
+
+ if ssb {
+ HStack {
+
+ Menu {
+
+ /*
+ Button {
+ showSettings.toggle()
+
+ } label: {
+ Label {
+ Text("Game Settings")
+ } icon: {
+ Image(systemName: "gearshape.circle")
+ }
+ }
+ */
+
+ Button {
+ pause_emulation(pauseEmu)
+ pauseEmu.toggle()
+ } label: {
+ Label {
+ Text(pauseEmu ? "Pause" : "Play")
+ } icon: {
+ Image(systemName: pauseEmu ? "pause.circle" : "play.circle")
+ }
+ }
+
+ 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)
+ }
+ .padding()
+
+ Spacer()
+
+ }
+ }
+
+ Spacer()
+
+ }
+ }
+ }
+ .onAppear {
+ LocationManager.sharedInstance.startUpdatingLocation()
+ Air.shared.connectionCallbacks.append { cool in
+ DispatchQueue.main.async {
+ isAirplaying = cool
+ // print(cool)
+ }
+ }
+
+ RegisterCallback("exit-emulation") { cool in
+ DispatchQueue.main.async {
+ print(cool)
+ startgame = nil
+ stop_emulation()
+ try? Ryujinx.shared.stop()
+ }
+ }
+ }
+ .onChange(of: scenePhase) { newPhase in
+ // Detect when the app enters the background
+ if newPhase == .background {
+ pause_emulation(true)
+ isInBackground = true
+ } else if newPhase == .active {
+ pause_emulation(false)
+ isInBackground = false
+ } else if newPhase == .inactive {
+ pause_emulation(true)
+ isInBackground = true
+ }
+ }
+ .sheet(isPresented: $showSettings) {
+ // PerGameSettingsView(titleId: startgame?.titleId ?? "", manager: InGameSettingsManager.shared)
+ // .onDisappear() {
+ // InGameSettingsManager.shared.saveSettings()
+ // }
+ }
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/InGameSettingsManager/InGameSettingsManager.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/InGameSettingsManager/InGameSettingsManager.swift
new file mode 100644
index 000000000..acbb15195
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/InGameSettingsManager/InGameSettingsManager.swift
@@ -0,0 +1,61 @@
+//
+// InGameSettingsManager.swift
+// MeloNX
+//
+// Created by Stossy11 on 12/06/2025.
+//
+
+import Foundation
+
+class InGameSettingsManager: PerGameSettingsManaging {
+ @Published var config: [String: Ryujinx.Arguments]
+
+ private var saveWorkItem: DispatchWorkItem?
+
+ public static var shared = InGameSettingsManager()
+
+ private init() {
+ self.config = PerGameSettingsManager.loadSettings() ?? [:]
+ }
+
+ func debouncedSave() {
+ saveWorkItem?.cancel()
+
+ let workItem = DispatchWorkItem { [weak self] in
+ guard let self = self else { return }
+ self.saveSettings()
+ }
+
+ saveWorkItem = workItem
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: workItem)
+ }
+
+ func saveSettings() {
+ if let currentgame = Ryujinx.shared.games.first(where: { $0.fileURL == URL(string: Ryujinx.shared.config?.gamepath ?? "") }) {
+ Ryujinx.shared.config = config[currentgame.titleId]
+ let args = Ryujinx.shared.buildCommandLineArgs(from: config[currentgame.titleId] ?? Ryujinx.Arguments())
+
+ let cArgs = args.map { strdup($0) }
+ defer { cArgs.forEach { free($0) } }
+ var argvPtrs = cArgs
+
+ let result = update_settings_external(Int32(args.count), &argvPtrs)
+
+ print(result)
+ }
+ }
+
+ static func loadSettings() -> [String: Ryujinx.Arguments]? {
+ var cool: [String: Ryujinx.Arguments] = [:]
+ if let currentgame = Ryujinx.shared.games.first(where: { $0.fileURL == URL(string: Ryujinx.shared.config?.gamepath ?? "") }) {
+ cool[currentgame.titleId] = Ryujinx.shared.config
+ return cool
+ } else {
+ return nil
+ }
+ }
+
+ func loadSettings() {
+ self.config = PerGameSettingsManager.loadSettings() ?? [:]
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/PerformanceDisplay/PerformanceOverlay.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/PerformanceDisplay/PerformanceOverlay.swift
new file mode 100644
index 000000000..5573e1282
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/PerformanceDisplay/PerformanceOverlay.swift
@@ -0,0 +1,70 @@
+//
+// Untitled.swift
+// MeloNX
+//
+// Created by Stossy11 on 21/12/2024.
+//
+
+import SwiftUI
+
+struct PerformanceOverlayView: View {
+ @StateObject private var memorymonitor = MemoryUsageMonitor()
+
+ @StateObject private var fpsmonitor = FPSMonitor()
+
+ var body: some View {
+ VStack {
+ Text("\(fpsmonitor.formatFPS())")
+ .foregroundStyle(.white)
+ .stroke(color: .black, width: 2)
+ Text(memorymonitor.formatMemorySize(memorymonitor.memoryUsage))
+ .foregroundStyle(.white)
+ .stroke(color: .black, width: 2)
+ }
+ }
+}
+
+extension View {
+ func stroke(color: Color, width: CGFloat = 1) -> some View {
+ modifier(StrokeModifier(strokeSize: width, strokeColor: color))
+ }
+}
+
+struct StrokeModifier: ViewModifier {
+ private let id = UUID()
+ var strokeSize: CGFloat = 1
+ var strokeColor: Color = .blue
+
+ func body(content: Content) -> some View {
+ if strokeSize > 0 {
+ appliedStrokeBackground(content: content)
+ } else {
+ content
+ }
+ }
+
+ private func appliedStrokeBackground(content: Content) -> some View {
+ content
+ .padding(strokeSize*2)
+ .background(
+ Rectangle()
+ .foregroundColor(strokeColor)
+ .mask(alignment: .center) {
+ mask(content: content)
+ }
+ )
+ }
+
+ func mask(content: Content) -> some View {
+ Canvas { context, size in
+ context.addFilter(.alphaThreshold(min: 0.01))
+ if let resolvedView = context.resolveSymbol(id: id) {
+ context.draw(resolvedView, at: .init(x: size.width/2, y: size.height/2))
+ }
+ } symbols: {
+ content
+ .tag(id)
+ .blur(radius: strokeSize)
+ }
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MeloMTKView.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MeloMTKView.swift
new file mode 100644
index 000000000..25d296012
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MeloMTKView.swift
@@ -0,0 +1,195 @@
+//
+// MeloMTKView.swift
+// MeloNX
+//
+// Created by Stossy11 on 03/03/2025.
+//
+
+import MetalKit
+import UIKit
+
+class MeloMTKView: MTKView {
+ private var activeTouches: [UITouch] = []
+ private var ignoredTouches: Set = []
+ private var touchIndexMap: [UITouch: Int] = [:]
+
+ private let baseWidth: CGFloat = 1280
+ private let baseHeight: CGFloat = 720
+ private var aspectRatio: AspectRatio = .fixed16x9
+
+ func setAspectRatio(_ ratio: AspectRatio) {
+ self.aspectRatio = ratio
+ }
+
+ private func scaleToTargetResolution(_ location: CGPoint) -> CGPoint? {
+ let viewWidth = self.frame.width
+ let viewHeight = self.frame.height
+
+ var scaleX: CGFloat
+ var scaleY: CGFloat
+ var offsetX: CGFloat = 0
+ var offsetY: CGFloat = 0
+
+ var targetAspect: CGFloat
+
+ switch aspectRatio {
+ case .fixed4x3:
+ targetAspect = 4.0 / 3.0
+ case .fixed16x9:
+ targetAspect = 16.0 / 9.0
+ case .fixed16x10:
+ targetAspect = 16.0 / 10.0
+ case .fixed21x9:
+ targetAspect = 21.0 / 9.0
+ case .fixed32x9:
+ targetAspect = 32.0 / 9.0
+ case .stretched:
+ scaleX = baseWidth / viewWidth
+ scaleY = baseHeight / viewHeight
+
+ let adjustedX = location.x
+ let adjustedY = location.y
+
+ let scaledX = max(0, min(adjustedX * scaleX, baseWidth))
+ let scaledY = max(0, min(adjustedY * scaleY, baseHeight))
+
+ return CGPoint(x: scaledX, y: scaledY)
+ }
+
+ let viewAspect = viewWidth / viewHeight
+
+ if viewAspect > targetAspect {
+ let scaledWidth = viewHeight * targetAspect
+ offsetX = (viewWidth - scaledWidth) / 2
+ scaleX = baseWidth / scaledWidth
+ scaleY = baseHeight / viewHeight
+ } else {
+ let scaledHeight = viewWidth / targetAspect
+ offsetY = (viewHeight - scaledHeight) / 2
+ scaleX = baseWidth / viewWidth
+ scaleY = baseHeight / scaledHeight
+ }
+
+ if location.x < offsetX || location.x > (viewWidth - offsetX) ||
+ location.y < offsetY || location.y > (viewHeight - offsetY) {
+ return nil
+ }
+
+ let adjustedX = location.x - offsetX
+ let adjustedY = location.y - offsetY
+
+ let scaledX = max(0, min(adjustedX * scaleX, baseWidth))
+ let scaledY = max(0, min(adjustedY * scaleY, baseHeight))
+
+ return CGPoint(x: scaledX, y: scaledY)
+ }
+
+ private func getNextAvailableIndex() -> Int {
+ for i in 0.., with event: UIEvent?) {
+ super.touchesBegan(touches, with: event)
+
+ let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
+ guard !disabled else { return }
+
+ setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
+
+ for touch in touches {
+ let location = touch.location(in: self)
+ guard let scaledLocation = scaleToTargetResolution(location) else {
+ ignoredTouches.insert(touch)
+ continue
+ }
+
+ let index = getNextAvailableIndex()
+ touchIndexMap[touch] = index
+ activeTouches.append(touch)
+
+ touch_began(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index))
+ }
+ }
+
+ override func touchesEnded(_ touches: Set, with event: UIEvent?) {
+ super.touchesEnded(touches, with: event)
+
+ let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
+ guard !disabled else {
+ for touch in touches {
+ ignoredTouches.remove(touch)
+ if let index = activeTouches.firstIndex(of: touch) {
+ activeTouches.remove(at: index)
+ }
+ touchIndexMap.removeValue(forKey: touch)
+ }
+ return
+ }
+
+ for touch in touches {
+ if ignoredTouches.remove(touch) != nil {
+ continue
+ }
+
+ if let touchIndex = touchIndexMap[touch] {
+ touch_ended(Int32(touchIndex))
+
+ if let arrayIndex = activeTouches.firstIndex(of: touch) {
+ activeTouches.remove(at: arrayIndex)
+ }
+ touchIndexMap.removeValue(forKey: touch)
+ }
+ }
+ }
+
+ override func touchesMoved(_ touches: Set, with event: UIEvent?) {
+ super.touchesMoved(touches, with: event)
+
+ let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
+ guard !disabled else { return }
+
+ setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
+
+ for touch in touches {
+ if ignoredTouches.contains(touch) {
+ continue
+ }
+
+ guard let touchIndex = touchIndexMap[touch] else {
+ continue
+ }
+
+ let location = touch.location(in: self)
+ guard let scaledLocation = scaleToTargetResolution(location) else {
+ touch_ended(Int32(touchIndex))
+
+ if let arrayIndex = activeTouches.firstIndex(of: touch) {
+ activeTouches.remove(at: arrayIndex)
+ }
+ touchIndexMap.removeValue(forKey: touch)
+ ignoredTouches.insert(touch)
+ continue
+ }
+
+ touch_moved(Float(scaledLocation.x), Float(scaledLocation.y), Int32(touchIndex))
+ }
+ }
+
+ override func touchesCancelled(_ touches: Set, with event: UIEvent?) {
+ super.touchesCancelled(touches, with: event)
+ touchesEnded(touches, with: event)
+ }
+
+
+ func resetTouchTracking() {
+ activeTouches.removeAll()
+ ignoredTouches.removeAll()
+ touchIndexMap.removeAll()
+ }
+}
+
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MetalView.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MetalView.swift
new file mode 100644
index 000000000..9f9576bb0
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MetalView.swift
@@ -0,0 +1,55 @@
+//
+// MetalView.swift
+// MeloNX
+//
+// Created by Stossy11 on 09/02/2025.
+//
+
+import SwiftUI
+import MetalKit
+
+struct MetalView: UIViewRepresentable {
+
+ var airplay: Bool = Air.shared.connected // just in case :3
+
+ func makeUIView(context: Context) -> UIView {
+
+ if Ryujinx.shared.emulationUIView == nil {
+ let view = MeloMTKView()
+
+ guard let metalLayer = view.layer as? CAMetalLayer else {
+ fatalError("[Swift] Error: MTKView's layer is not a CAMetalLayer")
+ }
+
+ metalLayer.device = MTLCreateSystemDefaultDevice()
+
+ let layerPtr = Unmanaged.passUnretained(metalLayer).toOpaque()
+ set_native_window(layerPtr)
+
+ Ryujinx.shared.emulationUIView = view
+
+
+ Ryujinx.shared.metalLayer = metalLayer
+
+ return view
+ }
+
+ if Double(UIDevice.current.systemVersion)! < 17.0 {
+
+ let uiview = MTKView()
+ let layer = Ryujinx.shared.metalLayer!
+
+ layer.frame = uiview.bounds
+
+ uiview.layer.addSublayer(layer)
+
+ return uiview
+ } else {
+ return Ryujinx.shared.emulationUIView!
+ }
+ }
+
+ func updateUIView(_ uiView: UIView, context: Context) {
+ // nothin
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/TouchView.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/TouchView.swift
new file mode 100644
index 000000000..e4cbaa44e
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/TouchView.swift
@@ -0,0 +1,18 @@
+//
+// TouchView.swift
+// MeloNX
+//
+// Created by Stossy11 on 05/03/2025.
+//
+
+import SwiftUI
+import MetalKit
+
+struct TouchView: UIViewRepresentable {
+ func makeUIView(context: Context) -> UIView {
+ let view = MeloMTKView()
+ return view
+ }
+
+ func updateUIView(_ uiView: UIView, context: Context) {}
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/ContentView.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/ContentView.swift
new file mode 100644
index 000000000..1eb05187d
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/ContentView.swift
@@ -0,0 +1,581 @@
+//
+// ContentView.swift
+// MeloNX
+//
+// Created by Stossy11 on 3/11/2024.
+//
+
+import SwiftUI
+import GameController
+import Darwin
+import UIKit
+import MetalKit
+import CoreLocation
+
+struct MoltenVKSettings: Codable, Hashable {
+ let string: String
+ var value: String
+}
+
+struct ContentView: View {
+ // MARK: - Properties
+
+ // Games
+ @State private var game: Game?
+
+ // Controllers
+ @State private var controllersList: [Controller] = []
+ @State private var currentControllers: [Controller] = []
+ @State var onscreencontroller: Controller = Controller(id: "", name: "")
+ @State var nativeControllers: [GCController: NativeController] = [:]
+ @State private var isVirtualControllerActive: Bool = false
+ @AppStorage("isVirtualController") var isVCA: Bool = true
+
+ // Settings and Configuration
+ private var config: Ryujinx.Arguments {
+ settingsManager.config
+ }
+
+ @StateObject private var settingsManager = SettingsManager.shared
+
+ @State var settings: [MoltenVKSettings]
+
+ // JIT
+ @AppStorage("useTrollStore") var useTrollStore: Bool = false
+ @AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
+ @AppStorage("stikJIT") var stikJIT: Bool = false
+
+ // Other Configuration
+ @State var isMK8: Bool = false
+ @AppStorage("quit") var quit: Bool = false
+ @State var quits: Bool = false
+ @AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = true
+ @AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = false
+ @AppStorage("ignoreJIT") var ignoreJIT: Bool = false
+
+ // Loading Animation
+ @AppStorage("showlogsloading") var showlogsloading: Bool = true
+ @State private var clumpOffset: CGFloat = -100
+ private let clumpWidth: CGFloat = 100
+ private let animationDuration: Double = 1.0
+ @State private var isAnimating = false
+ @State var isLoading = true
+
+
+ // MARK: - CORE
+ @StateObject var ryujinx = Ryujinx.shared
+
+ // MARK: - SDL
+ var sdlInitFlags: UInt32 = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO
+
+ // MARK: - Initialization
+ init() {
+ let defaultSettings: [MoltenVKSettings] = [
+ MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"),
+ MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "1"),
+ MoltenVKSettings(string: "MVK_DEBUG", value: "0"),
+ MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "0"),
+ MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "0"),
+ MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "512"),
+ ]
+
+ _settings = State(initialValue: defaultSettings)
+
+ initializeSDL()
+ }
+
+ // MARK: - Body
+ var body: some View {
+ if game != nil && (ryujinx.jitenabled || ignoreJIT) {
+ gameView
+ } else if game != nil && !ryujinx.jitenabled {
+ jitErrorView
+ } else {
+ mainMenuView
+ }
+ }
+
+ // MARK: - View Components
+
+ private var gameView: some View {
+ ZStack {
+ if #available(iOS 16, *) {
+ EmulationView(startgame: $game)
+ .persistentSystemOverlays(.hidden)
+ } else {
+ EmulationView(startgame: $game)
+ }
+
+ if isLoading {
+ ZStack {
+ Color.black.opacity(0.8)
+ emulationView.ignoresSafeArea(.all)
+ }
+ .edgesIgnoringSafeArea(.all)
+ .ignoresSafeArea(.all)
+ }
+ }
+ }
+
+ private var jitErrorView: some View {
+ Text("")
+ .fullScreenCover(isPresented:Binding(
+ get: { !ryujinx.jitenabled },
+ set: { newValue in
+ ryujinx.jitenabled = newValue
+
+ ryujinx.ryuIsJITEnabled()
+ })
+ ) {
+ JITPopover() {
+ ryujinx.jitenabled = false
+ }
+ }
+ }
+
+ private var mainMenuView: some View {
+ MainTabView(
+ startemu: $game,
+ MVKconfig: $settings,
+ controllersList: $controllersList,
+ currentControllers: $currentControllers,
+ onscreencontroller: $onscreencontroller
+ )
+ .onAppear {
+ quits = false
+ let _ = loadSettings()
+ isLoading = true
+
+ Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
+ refreshControllersList()
+ }
+
+ UserDefaults.standard.set(false, forKey: "lockInApp")
+
+ initControllerObservers()
+
+ Air.play(AnyView(
+ ControllerListView(game: $game)
+ ))
+
+ refreshControllersList()
+
+ ryujinx.addGames()
+
+ checkJitStatus()
+ }
+ .onOpenURL { url in
+ handleDeepLink(url)
+ }
+ }
+
+ private var emulationView: some View {
+ GeometryReader { screenGeometry in
+ ZStack {
+ gameLoadingContent(screenGeometry: screenGeometry)
+
+ HStack{
+
+ VStack {
+ if showlogsloading {
+ LogFileView(isfps: true)
+ .frame(alignment: .topLeading)
+ }
+
+ Spacer()
+ }
+
+ Spacer()
+ }
+ }
+ }
+ }
+
+ // MARK: - Helper Methods
+
+ private func gameLoadingContent(screenGeometry: GeometryProxy) -> some View {
+ HStack(spacing: screenGeometry.size.width * 0.04) {
+ if let icon = game?.icon {
+ Image(uiImage: icon)
+ .resizable()
+ .frame(
+ width: min(screenGeometry.size.width * 0.25, 250),
+ height: min(screenGeometry.size.width * 0.25, 250)
+ )
+ .clipShape(RoundedRectangle(cornerRadius: 16))
+ .shadow(color: .black.opacity(0.5), radius: 10, x: 0, y: 5)
+ }
+
+ VStack(alignment: .leading, spacing: screenGeometry.size.height * 0.015) {
+ Text("Loading \(game?.titleName ?? "Game")")
+ .font(.system(size: min(screenGeometry.size.width * 0.04, 32)))
+ .foregroundColor(.white)
+
+ loadingProgressBar(screenGeometry: screenGeometry)
+ }
+ }
+ .padding(.horizontal, screenGeometry.size.width * 0.06)
+ .padding(.vertical, screenGeometry.size.height * 0.05)
+ .position(
+ x: screenGeometry.size.width / 2,
+ y: screenGeometry.size.height * 0.5
+ )
+ }
+
+ private func loadingProgressBar(screenGeometry: GeometryProxy) -> some View {
+ GeometryReader { geometry in
+ let containerWidth = min(screenGeometry.size.width * 0.35, 350)
+
+ ZStack(alignment: .leading) {
+ Rectangle()
+ .cornerRadius(10)
+ .frame(width: containerWidth, height: min(screenGeometry.size.height * 0.015, 12))
+ .foregroundColor(.gray.opacity(0.3))
+ .shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2)
+
+ Rectangle()
+ .cornerRadius(10)
+ .frame(width: clumpWidth, height: min(screenGeometry.size.height * 0.015, 12))
+ .foregroundColor(.blue)
+ .shadow(color: .blue.opacity(0.5), radius: 4, x: 0, y: 2)
+ .offset(x: isAnimating ? containerWidth : -clumpWidth)
+ .animation(
+ Animation.linear(duration: 1.0)
+ .repeatForever(autoreverses: false),
+ value: isAnimating
+ )
+ }
+ .clipShape(RoundedRectangle(cornerRadius: 16))
+ .onAppear {
+ isAnimating = true
+ setupEmulation()
+
+ Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
+ if get_current_fps() != 0 {
+ withAnimation {
+ isLoading = false
+ isAnimating = false
+ }
+ timer.invalidate()
+ }
+ }
+ }
+ }
+ .frame(height: min(screenGeometry.size.height * 0.015, 12))
+ .frame(width: min(screenGeometry.size.width * 0.35, 350))
+ }
+
+ private func initializeSDL() {
+ setMoltenVKSettings()
+ SDL_SetMainReady()
+ SDL_iPhoneSetEventPump(SDL_TRUE)
+ SDL_Init(sdlInitFlags)
+ initialize()
+ }
+
+ private func initControllerObservers() {
+ NotificationCenter.default.addObserver(
+ forName: .GCControllerDidConnect,
+ object: nil,
+ queue: .main
+ ) { notification in
+ if let controller = notification.object as? GCController {
+ nativeControllers[controller] = .init(controller)
+ refreshControllersList()
+ }
+ }
+
+ NotificationCenter.default.addObserver(
+ forName: .GCControllerDidDisconnect,
+ object: nil,
+ queue: .main
+ ) { notification in
+ if let controller = notification.object as? GCController {
+ currentControllers = []
+ controllersList = []
+ nativeControllers[controller]?.cleanup()
+ nativeControllers[controller] = nil
+ refreshControllersList()
+ }
+ }
+ }
+
+ private func setupEmulation() {
+ isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil)
+
+ DispatchQueue.main.async {
+ start(displayid: 1)
+ }
+ }
+
+ private func refreshControllersList() {
+ currentControllers = []
+ controllersList = []
+
+ controllersList = ryujinx.getConnectedControllers()
+
+ if let onscreen = controllersList.first(where: { $0.name == ryujinx.virtualController.controllername }) {
+ self.onscreencontroller = onscreen
+ }
+
+ controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) })
+ controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") }
+
+ if controllersList.count == 1 {
+ currentControllers.append(controllersList[0])
+ } else if (controllersList.count - 1) >= 1 {
+ for controller in controllersList {
+ if controller.id != onscreencontroller.id && !currentControllers.contains(where: { $0.id == controller.id }) {
+ currentControllers.append(controller)
+ }
+ }
+ }
+ }
+
+ private func registerMotionForMatchingControllers() {
+ // Loop through currentControllers with index
+ for (index, controller) in currentControllers.enumerated() {
+ let slot = UInt8(index)
+
+ // Check native controllers
+ for (_, nativeController) in nativeControllers where nativeController.controllername == String("GC - \(controller.name)") && nativeController.tryGetMotionProvider() == nil {
+ nativeController.tryRegisterMotion(slot: slot)
+ continue
+ }
+
+ // Check virtual controller if active
+ if Ryujinx.shared.virtualController.controllername == controller.name && Ryujinx.shared.virtualController.tryGetMotionProvider() == nil {
+ Ryujinx.shared.virtualController.tryRegisterMotion(slot: slot)
+ continue
+ }
+ }
+ }
+
+ @StateObject private var persettings = PerGameSettingsManager.shared
+ private func start(displayid: UInt32) {
+ guard let game else { return }
+ var config = self.config
+
+ persettings.loadSettings()
+
+ if let customgame = persettings.config[game.titleId] {
+ config = customgame
+ }
+
+ config.gamepath = game.fileURL.path
+ config.inputids = Array(Set(currentControllers.map(\.id)))
+
+ configureEnvironmentVariables()
+
+ registerMotionForMatchingControllers()
+
+ config.inputids.isEmpty ? config.inputids.append("0") : ()
+
+ // Local DSU loopback to ryujinx per input id
+ for _ in config.inputids {
+ config.inputDSUServers.append("127.0.0.1:26760")
+ }
+
+ do {
+ try ryujinx.start(with: config)
+ } catch {
+ // print("Error: \(error.localizedDescription)")
+ }
+ }
+
+ private func configureEnvironmentVariables() {
+ if mVKPreFillBuffer {
+ mVKPreFillBuffer = false
+ // setenv("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", "2", 1)
+ }
+
+ if syncqsubmits {
+ setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "1", 1)
+ }
+ }
+
+ private func setMoltenVKSettings() {
+ settings.forEach { setting in
+ setenv(setting.string, setting.value, 1)
+ }
+ }
+
+ private func checkJitStatus() {
+ ryujinx.ryuIsJITEnabled()
+ if jitStreamerEB {
+ jitStreamerEB = false // byee jitstreamer eb
+ }
+
+
+ if !ryujinx.jitenabled {
+ if useTrollStore {
+ askForJIT()
+ } else if stikJIT {
+ enableJITStik()
+ } else if jitStreamerEB {
+ enableJITEB()
+ } else {
+ if !allocateTest(), checkDebugged() {
+ loop_heartbeat()
+ sleep(5)
+ let cool = String(cString: attach(getpid())!)
+ print(cool)
+ }
+ }
+ }
+ }
+
+ private func handleDeepLink(_ url: URL) {
+ if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
+ components.host == "game" {
+
+ refreshControllersList()
+
+ if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
+ game = ryujinx.games.first(where: { $0.titleId == text })
+ } else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
+ game = ryujinx.games.first(where: { $0.titleName == text })
+ }
+ }
+ }
+}
+
+extension Array {
+ @inlinable public mutating func mutableForEach(_ body: (inout Element) throws -> Void) rethrows {
+ for index in self.indices {
+ try body(&self[index])
+ }
+ }
+}
+
+class LocationManager: NSObject, CLLocationManagerDelegate {
+
+ private var locationManager: CLLocationManager
+
+ static let sharedInstance = LocationManager()
+
+ private override init() {
+ locationManager = CLLocationManager()
+ super.init()
+ locationManager.delegate = self
+ locationManager.desiredAccuracy = kCLLocationAccuracyBest
+ locationManager.pausesLocationUpdatesAutomatically = false
+ }
+
+ func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
+ // print("wow")
+ }
+
+ func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
+ print("Location manager failed with: \(error)")
+ }
+
+ func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
+ if manager.authorizationStatus == .denied {
+ print("Location services are disabled in settings.")
+ } else {
+ startUpdatingLocation()
+ }
+ }
+
+ func stop() {
+ if UserDefaults.standard.bool(forKey: "location-enabled") {
+ locationManager.stopUpdatingLocation()
+ }
+ }
+
+ func startUpdatingLocation() {
+ if UserDefaults.standard.bool(forKey: "location-enabled") {
+ locationManager.requestAlwaysAuthorization()
+ locationManager.allowsBackgroundLocationUpdates = true
+ locationManager.startUpdatingLocation()
+ }
+ }
+}
+
+struct ControllerListView: View {
+ @State private var selectedIndex = 0
+ @Binding var game: Game?
+ @ObservedObject private var ryujinx = Ryujinx.shared
+
+ var body: some View {
+ List(ryujinx.games.indices, id: \.self) { index in
+ let game = ryujinx.games[index]
+
+ HStack(spacing: 16) {
+ // Game Icon
+ Group {
+ if let icon = game.icon {
+ Image(uiImage: icon)
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ } else {
+ ZStack {
+ RoundedRectangle(cornerRadius: 10)
+ Image(systemName: "gamecontroller.fill")
+ .font(.system(size: 24))
+ .foregroundColor(.gray)
+ }
+ }
+ }
+ .frame(width: 55, height: 55)
+ .cornerRadius(10)
+
+ // Game Info
+ VStack(alignment: .leading, spacing: 4) {
+ Text(game.titleName)
+ .font(.system(size: 16, weight: .medium))
+ .foregroundColor(.primary)
+
+ HStack(spacing: 4) {
+ Text(game.developer)
+
+ if !game.version.isEmpty && game.version != "0" {
+ Text("•")
+ Text("v\(game.version)")
+ }
+ }
+ .font(.system(size: 14))
+ .foregroundColor(.secondary)
+ }
+
+ Spacer()
+ }
+ .background(selectedIndex == index ? Color.blue.opacity(0.3) : .clear)
+ }
+ .onAppear(perform: setupControllerObservers)
+ }
+
+ private func setupControllerObservers() {
+ let dpadHandler: GCControllerDirectionPadValueChangedHandler = { _, _, yValue in
+ if yValue == 1.0 {
+ selectedIndex = max(0, selectedIndex - 1)
+ } else if yValue == -1.0 {
+ selectedIndex = min(ryujinx.games.count - 1, selectedIndex + 1)
+ }
+ }
+
+ for controller in GCController.controllers() {
+ print("Controller connected: \(controller.vendorName ?? "Unknown")")
+ controller.playerIndex = .index1
+
+ controller.microGamepad?.dpad.valueChangedHandler = dpadHandler
+ controller.extendedGamepad?.dpad.valueChangedHandler = dpadHandler
+
+ controller.extendedGamepad?.buttonA.pressedChangedHandler = { _, _, pressed in
+ if pressed {
+ print("A button pressed")
+ game = ryujinx.games[selectedIndex]
+ }
+ }
+ }
+
+ NotificationCenter.default.addObserver(
+ forName: .GCControllerDidConnect,
+ object: nil,
+ queue: .main
+ ) { _ in
+ setupControllerObservers()
+ }
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameCompatibility/GameCompatibilityCache.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameCompatibility/GameCompatibilityCache.swift
new file mode 100644
index 000000000..4b757ff35
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameCompatibility/GameCompatibilityCache.swift
@@ -0,0 +1,44 @@
+//
+// GameRequirementsCache.swift
+// MeloNX
+//
+// Created by Stossy11 on 21/03/2025.
+//
+
+
+import Foundation
+
+class GameCompatibiliryCache {
+ static let shared = GameCompatibiliryCache()
+ private let cacheKey = "gameRequirementsCache"
+ private let timestampKey = "gameRequirementsCacheTimestamp"
+
+ private let cacheDuration: TimeInterval = Double.random(in: 3...5) * 24 * 60 * 60 // Randomly pick 3-5 days
+
+ func getCachedData() -> [GameRequirements]? {
+ guard let cachedData = UserDefaults.standard.data(forKey: cacheKey),
+ let timestamp = UserDefaults.standard.object(forKey: timestampKey) as? Date else {
+ return nil
+ }
+
+ let timeElapsed = Date().timeIntervalSince(timestamp)
+ if timeElapsed > cacheDuration {
+ clearCache()
+ return nil
+ }
+
+ return try? JSONDecoder().decode([GameRequirements].self, from: cachedData)
+ }
+
+ func setCachedData(_ data: [GameRequirements]) {
+ if let encodedData = try? JSONEncoder().encode(data) {
+ UserDefaults.standard.set(encodedData, forKey: cacheKey)
+ UserDefaults.standard.set(Date(), forKey: timestampKey)
+ }
+ }
+
+ func clearCache() {
+ UserDefaults.standard.removeObject(forKey: cacheKey)
+ UserDefaults.standard.removeObject(forKey: timestampKey)
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/GamesList/GameInfoSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameInfoSheet.swift
similarity index 85%
rename from src/MeloNX/MeloNX/App/Views/GamesList/GameInfoSheet.swift
rename to src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameInfoSheet.swift
index 5c4f9c3c8..c6cbb3880 100644
--- a/src/MeloNX/MeloNX/App/Views/GamesList/GameInfoSheet.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameInfoSheet.swift
@@ -10,7 +10,7 @@ import SwiftUI
struct GameInfoSheet: View {
let game: Game
- @Environment(\.dismiss) var dismiss
+ @Environment(\.presentationMode) var presentationMode
var body: some View {
iOSNav {
@@ -44,7 +44,7 @@ struct GameInfoSheet: View {
.multilineTextAlignment(.center)
Text(game.developer)
.font(.caption)
- .foregroundStyle(.secondary)
+ .foregroundColor(.secondary)
}
.padding(.vertical, 3)
}
@@ -56,7 +56,7 @@ struct GameInfoSheet: View {
Text("**Version**")
Spacer()
Text(game.version)
- .foregroundStyle(Color.secondary)
+ .foregroundColor(Color.secondary)
}
HStack {
Text("**Title ID**")
@@ -69,36 +69,36 @@ struct GameInfoSheet: View {
}
Spacer()
Text(game.titleId)
- .foregroundStyle(Color.secondary)
+ .foregroundColor(Color.secondary)
}
HStack {
Text("**Game Size**")
Spacer()
Text("\(fetchFileSize(for: game.fileURL) ?? 0) bytes")
- .foregroundStyle(Color.secondary)
+ .foregroundColor(Color.secondary)
}
HStack {
Text("**File Type**")
Spacer()
Text(getFileType(game.fileURL))
- .foregroundStyle(Color.secondary)
+ .foregroundColor(Color.secondary)
}
VStack(alignment: .leading, spacing: 4) {
Text("**Game URL**")
Text(trimGameURL(game.fileURL))
- .foregroundStyle(Color.secondary)
+ .foregroundColor(Color.secondary)
}
} header: {
Text("Information")
}
- .headerProminence(.increased)
+ // .headerProminence(.increased)
}
.navigationTitle(game.titleName)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
- ToolbarItem(placement: .navigationBarTrailing) {
- Button("Done") {
- dismiss()
+ ToolbarItem(placement: .cancellationAction) {
+ Button("Dismiss") {
+ presentationMode.wrappedValue.dismiss()
}
}
}
@@ -113,7 +113,7 @@ struct GameInfoSheet: View {
return size
}
} catch {
- print("Error getting file size: \(error)")
+ // print("Error getting file size: \(error)")
}
return nil
}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameListView.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameListView.swift
new file mode 100644
index 000000000..47124f48e
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameListView.swift
@@ -0,0 +1,1267 @@
+//
+// GameListView.swift
+// MeloNX
+//
+// Created by Stossy11 on 3/11/2024.
+//
+
+import SwiftUI
+import UniformTypeIdentifiers
+
+extension UTType {
+ static let nsp = UTType(exportedAs: "com.nintendo.switch-package")
+ static let xci = UTType(exportedAs: "com.nintendo.switch-cartridge")
+}
+
+struct GameLibraryView: View {
+ @Binding var startemu: Game?
+ @State private var searchText = ""
+ @State private var isSearching = false
+ @AppStorage("recentGames") private var recentGamesData: Data = Data()
+ @State private var recentGames: [Game] = []
+ @Environment(\.colorScheme) var colorScheme
+ @State var firmwareInstaller = false
+ @State var firmwareversion = "0"
+ @State var isImporting: Bool = false
+ @State var startgame = false
+ @State var isSelectingGameFile = false
+ @State var isViewingGameInfo: Bool = false
+ @State var gamePerGameSettings: Game?
+ var isShowingPerGameSettings: Binding {
+ Binding {
+ gamePerGameSettings != nil
+ } set: { value in
+ !value ? gamePerGameSettings = nil : ()
+ }
+
+ }
+ @State var isSelectingGameUpdate: Bool = false
+ @State var isSelectingGameDLC: Bool = false
+ @StateObject var ryujinx = Ryujinx.shared
+ @State var gameInfo: Game?
+ @State var gameRequirements: [GameRequirements] = []
+ @State private var showingOptions = false
+
+ var games: Binding<[Game]> {
+ Binding(
+ get: { Ryujinx.shared.games },
+ set: { Ryujinx.shared.games = $0 }
+ )
+ }
+
+ var filteredGames: [Game] {
+ if searchText.isEmpty {
+ return Ryujinx.shared.games.filter { game in
+ !realRecentGames.contains(where: { $0.fileURL == game.fileURL })
+ }
+ }
+ return Ryujinx.shared.games.filter {
+ $0.titleName.localizedCaseInsensitiveContains(searchText) ||
+ $0.developer.localizedCaseInsensitiveContains(searchText)
+ }
+ }
+
+ var realRecentGames: [Game] {
+ let games = Ryujinx.shared.games
+ return recentGames.compactMap { recentGame in
+ games.first(where: { $0.fileURL == recentGame.fileURL })
+ }
+ }
+
+ var body: some View {
+ iOSNav {
+ ZStack {
+ // Background color
+ Color(UIColor.systemBackground)
+ .ignoresSafeArea()
+
+ VStack(spacing: 0) {
+ // Header with stats
+ if !Ryujinx.shared.games.isEmpty {
+ GameLibraryHeader(
+ totalGames: Ryujinx.shared.games.count,
+ recentGames: realRecentGames.count,
+ firmwareVersion: firmwareversion
+ )
+ }
+
+ // Game list
+ if Ryujinx.shared.games.isEmpty {
+ EmptyGameLibraryView(isSelectingGameFile: $isSelectingGameFile)
+ } else {
+ gameListView
+ .animation(.easeInOut(duration: 0.3), value: searchText)
+ }
+ }
+ }
+ .navigationTitle("Game Library")
+ .navigationBarTitleDisplayMode(.large)
+ .onAppear {
+ loadRecentGames()
+ firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion())
+
+ pullGameCompatibility() { result in
+ switch result {
+ case .success(let success):
+ gameRequirements = success
+ case .failure(_):
+ print("Failed to load game compatibility data")
+ }
+ }
+ }
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+
+ Button {
+ isSelectingGameFile = true
+ isImporting = true
+ } label: {
+ Label("Add Game", systemImage: "plus")
+ .labelStyle(.iconOnly)
+ .font(.system(size: 16, weight: .semibold))
+ }
+ // .buttonStyle(.bordered)
+ .accentColor(.blue)
+ }
+
+ ToolbarItem(placement: .topBarLeading) {
+ Menu {
+ firmwareSection
+
+ Divider()
+
+ Button {
+ isSelectingGameFile = false
+ isImporting = true
+ } label: {
+ Label("Open Game", systemImage: "square.and.arrow.down")
+ }
+
+ Button {
+ let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
+ var sharedurl = documentsUrl.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://")
+ if ProcessInfo.processInfo.isiOSAppOnMac {
+ sharedurl = documentsUrl.absoluteString
+ }
+ if UIApplication.shared.canOpenURL(URL(string: sharedurl)!) {
+ UIApplication.shared.open(URL(string: sharedurl)!, options: [:])
+ }
+ } label: {
+ Label("Show MeloNX Folder", systemImage: "folder")
+ }
+ } label: {
+ Label("Options", systemImage: "ellipsis.circle")
+ .labelStyle(.iconOnly)
+ .foregroundColor(.blue)
+ }
+ }
+ }
+ .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
+ guard let game else { return }
+ addToRecentGames(game)
+ }
+ // .searchable(text: $searchText, placement: .toolbar, prompt: "Search games or developers")
+ .onChange(of: searchText) { _ in
+ isSearching = !searchText.isEmpty
+ }
+ .onChange(of: isImporting) { newValue in
+ if newValue {
+ FileImporterManager.shared.importFiles(types: [.nsp, .xci, .item]) { result in
+ isImporting = false
+ handleRunningGame(result: result)
+ }
+ }
+ }
+ .onChange(of: isSelectingGameFile) { newValue in
+ if newValue {
+ FileImporterManager.shared.importFiles(types: [.nsp, .xci, .item]) { result in
+ isImporting = false
+ handleAddingGame(result: result)
+ }
+ }
+ }
+ .onChange(of: firmwareInstaller) { newValue in
+ if newValue {
+ FileImporterManager.shared.importFiles(types: [.folder, .zip]) { result in
+ isImporting = false
+ handleFirmwareImport(result: result)
+ }
+ }
+ }
+ .sheet(isPresented: $isSelectingGameUpdate) {
+ UpdateManagerSheet(game: $gameInfo)
+ }
+ .sheet(isPresented: $isSelectingGameDLC) {
+ DLCManagerSheet(game: $gameInfo)
+ }
+ .sheet(isPresented: isShowingPerGameSettings) {
+ PerGameSettingsView(titleId: gamePerGameSettings!.titleId)
+ }
+ .sheet(isPresented: Binding(
+ get: { isViewingGameInfo && gameInfo != nil },
+ set: { newValue in
+ if !newValue {
+ isViewingGameInfo = false
+ gameInfo = nil
+ }
+ }
+ )) {
+ if let game = gameInfo {
+ GameInfoSheet(game: game)
+ }
+ }
+ }
+ }
+
+ // MARK: - Subviews
+
+ private var gameListView: some View {
+ ScrollView {
+ LazyVStack(spacing: 0) {
+ if !isSearching && !realRecentGames.isEmpty {
+ // Recent Games Section
+ VStack(alignment: .leading, spacing: 0) {
+ Text("Recent Games")
+ .font(.headline)
+ .foregroundColor(.primary)
+ .padding(.horizontal)
+ .padding(.top)
+
+ ScrollView(.horizontal, showsIndicators: false) {
+ LazyHStack(spacing: 16) {
+ ForEach(realRecentGames) { game in
+ GameCardView(
+ game: game,
+ startemu: $startemu,
+ games: games,
+ isViewingGameInfo: $isViewingGameInfo,
+ isSelectingGameUpdate: $isSelectingGameUpdate,
+ isSelectingGameDLC: $isSelectingGameDLC,
+ gameRequirements: $gameRequirements,
+ gameInfo: $gameInfo
+ )
+ .contextMenu {
+ gameContextMenu(for: game)
+ }
+ }
+ }
+ .padding()
+ }
+ }
+
+ // Library Section
+ if !filteredGames.isEmpty {
+ VStack(alignment: .leading) {
+ Text("Library")
+ .font(.headline)
+ .foregroundColor(.primary)
+ .padding(.horizontal)
+ .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)
+ }
+ }
+
+ Spacer(minLength: 50)
+ }
+ }
+ }
+
+ private var firmwareSection: some View {
+ Group {
+ if firmwareversion == "0" {
+ Button {
+ DispatchQueue.main.async {
+ firmwareInstaller.toggle()
+ }
+ } label: {
+ Label("Install Firmware", systemImage: "square.and.arrow.down")
+ }
+
+ } else {
+ Menu("Applets") {
+ Button {
+ let game = Game(containerFolder: URL(string: "none")!, fileType: .item, fileURL: URL(string: "0x0100000000001009")!, titleName: "Mii Maker", titleId: "0", developer: "Nintendo", version: firmwareversion)
+ self.startemu = game
+ } label: {
+ Label("Launch Mii Maker", systemImage: "person.crop.circle")
+ }
+
+ Button {
+ let game = Game(containerFolder: URL(string: "none")!, fileType: .item, fileURL: URL(string: "0x0100000000001000")!, titleName: "Home Menu (Broken)", titleId: "0", developer: "Nintendo", version: firmwareversion)
+ self.startemu = game
+ } label: {
+ Label("Home Menu (Broken)", systemImage: "house.circle")
+ }
+ .foregroundStyle(.red)
+ }
+ }
+ }
+ }
+
+ // MARK: - Game Management Functions
+
+ private func addToRecentGames(_ game: Game) {
+ recentGames.removeAll { $0.titleId == game.titleId }
+ recentGames.insert(game, at: 0)
+
+ if recentGames.count > 5 {
+ recentGames = Array(recentGames.prefix(5))
+ }
+
+ saveRecentGames()
+ }
+
+ private func removeFromRecentGames(_ game: Game) {
+ recentGames.removeAll { $0.titleId == game.titleId }
+ saveRecentGames()
+ }
+
+ private func saveRecentGames() {
+ do {
+ let encoder = JSONEncoder()
+ let data = try encoder.encode(recentGames)
+ recentGamesData = data
+ } catch {
+ // print("Error saving recent games: \(error)")
+ }
+ }
+
+ private func loadRecentGames() {
+ do {
+ let decoder = JSONDecoder()
+ recentGames = try decoder.decode([Game].self, from: recentGamesData)
+ } catch {
+ // print("Error loading recent games: \(error)")
+ recentGames = []
+ }
+ }
+
+ private func deleteGame(game: Game) {
+ let fileManager = FileManager.default
+ do {
+ try fileManager.removeItem(at: game.fileURL)
+ Ryujinx.shared.games.removeAll { $0.id == game.id }
+ Ryujinx.shared.games = Ryujinx.shared.loadGames()
+ } catch {
+ // print("Error deleting game: \(error)")
+ }
+ }
+
+ // MARK: - Import Handlers
+
+ private func handleAddingGame(result: Result<[URL], Error>) {
+ switch result {
+ case .success(let urls):
+ guard let url = urls.first, url.startAccessingSecurityScopedResource() else {
+ // print("Failed to access security-scoped resource")
+ return
+ }
+ defer { url.stopAccessingSecurityScopedResource() }
+
+ do {
+ let fileManager = FileManager.default
+ let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
+ let romsDirectory = documentsDirectory.appendingPathComponent("roms")
+
+ if !fileManager.fileExists(atPath: romsDirectory.path) {
+ try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
+ }
+
+ let destinationURL = romsDirectory.appendingPathComponent(url.lastPathComponent)
+ try fileManager.copyItem(at: url, to: destinationURL)
+
+ Ryujinx.shared.games = Ryujinx.shared.loadGames()
+ } catch {
+ // print("Error copying game file: \(error)")
+ }
+ case .failure(let err):
+ print("File import failed: \(err.localizedDescription)")
+ }
+ }
+
+ private func handleRunningGame(result: Result<[URL], Error>) {
+ switch result {
+ case .success(let urls):
+ guard let url = urls.first, url.startAccessingSecurityScopedResource() else {
+ // print("Failed to access security-scoped resource")
+ return
+ }
+
+ do {
+ let handle = try FileHandle(forReadingFrom: url)
+ let fileExtension = (url.pathExtension as NSString).utf8String
+ let extensionPtr = UnsafeMutablePointer(mutating: fileExtension)
+
+ let gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
+
+ let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: url)
+
+ DispatchQueue.main.async {
+ startemu = game
+ }
+ } catch {
+ // print(error)
+ }
+
+ case .failure(let err):
+ print("File import failed: \(err.localizedDescription)")
+ }
+ }
+
+ private func handleFirmwareImport(result: Result<[URL], Error>) {
+ switch result {
+ case .success(let url):
+ guard let url = url.first else {
+ return
+ }
+
+ do {
+ let fun = url.startAccessingSecurityScopedResource()
+ let path = url.path
+
+ Ryujinx.shared.installFirmware(firmwarePath: path)
+
+ firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion())
+ if fun {
+ url.stopAccessingSecurityScopedResource()
+ }
+ }
+ case .failure(let error):
+ print(error)
+ }
+ }
+
+ // MARK: - Context Menus
+
+ private func gameContextMenu(for game: Game) -> some View {
+ Group {
+ Section {
+ Button {
+ startemu = game
+ } label: {
+ Label("Play Now", systemImage: "play.fill")
+ }
+
+ Button {
+ gameInfo = game
+ isViewingGameInfo.toggle()
+ } label: {
+ Label("Game Info", systemImage: "info.circle")
+ }
+
+ Button {
+ gamePerGameSettings = 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) {
+ removeFromRecentGames(game)
+ } label: {
+ Label("Remove from Recents", systemImage: "trash")
+ }
+
+ if #available(iOS 15, *) {
+ Button(role: .destructive) {
+ deleteGame(game: game)
+ } label: {
+ Label("Delete Game", systemImage: "trash")
+ }
+ } else {
+ Button(action: {
+ deleteGame(game: game)
+ }) {
+ Label("Delete Game", systemImage: "trash")
+ .foregroundColor(.red)
+ }
+ }
+
+ }
+ }
+ }
+}
+
+extension Game: Codable {
+ private enum CodingKeys: String, CodingKey {
+ case titleName, titleId, developer, version, fileURL, containerFolder, fileType
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ titleName = try container.decode(String.self, forKey: .titleName)
+ titleId = try container.decode(String.self, forKey: .titleId)
+ developer = try container.decode(String.self, forKey: .developer)
+ version = try container.decode(String.self, forKey: .version)
+ fileURL = try container.decode(URL.self, forKey: .fileURL)
+ containerFolder = try container.decode(URL.self, forKey: .containerFolder)
+ fileType = try container.decode(UTType.self, forKey: .fileType)
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(titleName, forKey: .titleName)
+ try container.encode(titleId, forKey: .titleId)
+ try container.encode(developer, forKey: .developer)
+ try container.encode(version, forKey: .version)
+ try container.encode(fileURL, forKey: .fileURL)
+ try container.encode(containerFolder, forKey: .containerFolder)
+ try container.encode(fileType, forKey: .fileType)
+ }
+}
+
+
+// MARK: - Empty Library View
+struct EmptyGameLibraryView: View {
+ @Binding var isSelectingGameFile: Bool
+
+ var body: some View {
+ VStack(spacing: 24) {
+ Spacer()
+
+ Image(systemName: "gamecontroller.fill")
+ .font(.system(size: 70))
+ .foregroundColor(.blue.opacity(0.7))
+ .padding(.bottom)
+
+ Text("No Games Found")
+ .font(.title2.bold())
+ .foregroundColor(.primary)
+
+ Text("Add ROM files to get started with your gaming experience")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.center)
+ .padding(.horizontal, 40)
+
+ Button {
+ isSelectingGameFile = true
+ } label: {
+ Label("Add Game", systemImage: "plus")
+ .font(.headline)
+ .padding(.horizontal, 24)
+ .padding(.vertical, 12)
+ .background(Color.blue)
+ .foregroundColor(.white)
+ .cornerRadius(10)
+ }
+ .padding(.top)
+
+ Spacer()
+ }
+ .padding()
+ }
+}
+
+// MARK: - Library Header
+struct GameLibraryHeader: View {
+ let totalGames: Int
+ let recentGames: Int
+ let firmwareVersion: String
+
+ var body: some View {
+ HStack(spacing: 16) {
+ // Stats cards
+ StatCard(
+ icon: "gamecontroller.fill",
+ title: "Total Games",
+ value: "\(totalGames)",
+ color: .blue
+ )
+
+ StatCard(
+ icon: "clock.fill",
+ title: "Recent",
+ value: "\(recentGames)",
+ color: .green
+ )
+
+ StatCard(
+ icon: "cpu",
+ title: "Firmware",
+ value: firmwareVersion == "0" ? "None" : firmwareVersion,
+ color: firmwareVersion == "0" ? .red : .orange
+ )
+ }
+ .padding(.horizontal)
+ .padding(.top, 8)
+ .padding(.bottom, 4)
+ }
+}
+
+struct StatCard: View {
+ let icon: String
+ let title: String
+ let value: String
+ let color: Color
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 4) {
+ HStack {
+ Image(systemName: icon)
+ .foregroundColor(color)
+ Text(title)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ Text(value)
+ .font(.system(size: 16, weight: .bold))
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(10)
+ .background(color.opacity(0.1))
+ .cornerRadius(10)
+ }
+}
+
+// MARK: - Game Card View
+struct GameCardView: View {
+ let game: Game
+ @Binding var startemu: Game?
+ @Binding var games: [Game]
+ @Binding var isViewingGameInfo: Bool
+ @Binding var isSelectingGameUpdate: Bool
+ @Binding var isSelectingGameDLC: Bool
+ @Binding var gameRequirements: [GameRequirements]
+ @Binding var gameInfo: Game?
+ @Environment(\.colorScheme) var colorScheme
+ let totalMemory = ProcessInfo.processInfo.physicalMemory
+
+ var gameRequirement: GameRequirements? {
+ gameRequirements.first(where: { $0.game_id == game.titleId })
+ }
+
+ var body: some View {
+ VStack(spacing: 0) {
+ // Game Icon
+ ZStack {
+ if let icon = game.icon {
+ Image(uiImage: icon)
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ .frame(width: 130, height: 130)
+ .cornerRadius(8)
+ } else {
+ RoundedRectangle(cornerRadius: 8)
+ .fill(colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6))
+ .frame(width: 130, height: 130)
+
+ Image(systemName: "gamecontroller.fill")
+ .font(.system(size: 40))
+ .foregroundColor(.gray)
+ }
+
+ // Play button overlay
+ Button {
+ startemu = game
+ } label: {
+ Circle()
+ .fill(Color.black.opacity(0.6))
+ .frame(width: 40, height: 40)
+ .overlay(
+ Image(systemName: "play.fill")
+ .font(.system(size: 16))
+ .foregroundColor(.white)
+ )
+ }
+ .offset(x: 0, y: 0)
+ .opacity(0.8)
+ }
+
+ // Game info
+ VStack(alignment: .leading, spacing: 4) {
+ Text(game.titleName)
+ .font(.system(size: 14, weight: .medium))
+ .multilineTextAlignment(.leading)
+ .foregroundColor(.primary)
+ .lineLimit(1)
+
+ Text(game.developer)
+ .font(.system(size: 12))
+ .foregroundColor(.secondary)
+ .lineLimit(1)
+
+ // Compatibility tag
+ if let req = gameRequirement {
+ HStack(spacing: 4) {
+ Text(req.compatibility)
+ .font(.system(size: 10, weight: .medium))
+ .foregroundColor(.white)
+ .padding(.horizontal, 6)
+ .padding(.vertical, 2)
+ .background(req.color)
+ .cornerRadius(4)
+
+ Text(req.device_memory)
+ .font(.system(size: 10, weight: .medium))
+ .foregroundColor(.white)
+ .padding(.horizontal, 6)
+ .padding(.vertical, 2)
+ .background(req.memoryInt <= Int(String(format: "%.0f", Double(totalMemory) / 1_000_000_000)) ?? 0 ? Color.blue : Color.red)
+ .cornerRadius(4)
+ }
+ } else {
+ HStack(spacing: 4) {
+ Text("0GB")
+ .font(.system(size: 10, weight: .medium))
+ .foregroundColor(.clear)
+ .padding(.horizontal, 6)
+ .padding(.vertical, 2)
+ .background(Color.clear)
+ .cornerRadius(4)
+ }
+ }
+ }
+ .frame(width: 130, alignment: .leading)
+ .padding(.top, 8)
+ }
+ .onTapGesture {
+ startemu = game
+ }
+ }
+}
+
+// MARK: - Game List Row
+struct GameListRow: View {
+ let game: Game
+ @Binding var startemu: Game?
+ @Binding var games: [Game]
+ @Binding var isViewingGameInfo: Bool
+ @Binding var isSelectingGameUpdate: Bool
+ @Binding var isSelectingGameDLC: Bool
+ @Binding var gameRequirements: [GameRequirements]
+ @Binding var gameInfo: Game?
+ @StateObject private var settingsManager = PerGameSettingsManager.shared
+ @Binding var perGameSettings: Game?
+ @State var gametoDelete: Game?
+ @State var showGameDeleteConfirmation: Bool = false
+ @Environment(\.colorScheme) var colorScheme
+ @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
+ @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
+
+ @AppStorage("portal") var gamepo = false
+
+ var body: some View {
+ if #available(iOS 15.0, *) {
+ Button(action: {
+ startemu = game
+ }) {
+ HStack(spacing: 16) {
+ // Game Icon
+ if let icon = game.icon {
+ Image(uiImage: icon)
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ .frame(width: 55, height: 55)
+ .cornerRadius(10)
+ } else {
+ ZStack {
+ RoundedRectangle(cornerRadius: 10)
+ .fill(colorScheme == .dark ?
+ Color(.systemGray5) : Color(.systemGray6))
+ .frame(width: 55, height: 55)
+
+ Image(systemName: "gamecontroller.fill")
+ .font(.system(size: 24))
+ .foregroundColor(.gray)
+ }
+ }
+
+ // Game Info
+ VStack(alignment: .leading, spacing: 4) {
+ Text(game.titleName)
+ .font(.system(size: 16, weight: .medium))
+ .foregroundColor(.primary)
+ .multilineTextAlignment(.leading)
+
+ HStack {
+ Text(game.developer)
+ .font(.system(size: 12))
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.leading)
+
+ if !game.version.isEmpty && game.version != "0" {
+ Divider().frame(width: 1, height: 15)
+
+ Text("v\(game.version)")
+ .font(.system(size: 10))
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+
+ 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()
+
+ VStack(alignment: .leading) {
+ // Compatibility badges
+ HStack {
+ if let gameReq = gameRequirements.first(where: { $0.game_id == game.titleId }) {
+ let totalMemory = ProcessInfo.processInfo.physicalMemory
+
+ HStack(spacing: 4) {
+ Text(gameReq.device_memory)
+ .font(.system(size: 11, weight: .medium))
+ .foregroundColor(.white)
+ .padding(.horizontal, 4)
+ .padding(.vertical, 4)
+ .background(
+ Capsule()
+ .fill(gameReq.memoryInt <= Int(String(format: "%.0f", Double(totalMemory) / 1_000_000_000)) ?? 0 ? Color.blue : Color.red)
+ )
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .fixedSize(horizontal: true, vertical: false)
+ .layoutPriority(1)
+
+ Text(gameReq.compatibility)
+ .font(.system(size: 11, weight: .medium))
+ .foregroundColor(.white)
+ .padding(.horizontal, 4)
+ .padding(.vertical, 4)
+ .background(
+ Capsule()
+ .fill(gameReq.color)
+ )
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .fixedSize(horizontal: true, vertical: false)
+ .layoutPriority(1)
+ }
+ }
+
+ // Play button
+ Image(systemName: "play.circle.fill")
+ .font(.title3)
+ .foregroundColor(.blue)
+ }
+ }
+ }
+ .padding(.horizontal, 10)
+ .padding(.vertical, 4)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+ .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) {
+ Button(role: .destructive) {
+ gametoDelete = game
+ showGameDeleteConfirmation.toggle()
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
+
+ Button {
+ gameInfo = game
+ isViewingGameInfo.toggle()
+ } label: {
+ Label("Info", systemImage: "info.circle")
+ }
+ .tint(.blue)
+ }
+ .swipeActions(edge: .leading) {
+ Button {
+ startemu = game
+ } label: {
+ Label("Play", systemImage: "play.fill")
+ }
+ .tint(.green)
+ }
+ .confirmationDialog("Are you sure you want to delete this game?", isPresented: $showGameDeleteConfirmation) {
+ Button("Delete", role: .destructive) {
+ if let game = gametoDelete {
+ deleteGame(game: game)
+ }
+ }
+ Button("Cancel", role: .cancel) {}
+ } message: {
+ Text("Are you sure you want to delete \(gametoDelete?.titleName ?? "this game")?")
+ }
+ .listRowInsets(EdgeInsets())
+ .wow(colorScheme)
+ } else {
+ Button(action: {
+ startemu = game
+ }) {
+ HStack(spacing: 16) {
+ // Game Icon
+ if let icon = game.icon {
+ Image(uiImage: icon)
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ .frame(width: 55, height: 55)
+ .cornerRadius(10)
+ } else {
+ ZStack {
+ RoundedRectangle(cornerRadius: 10)
+ .fill(colorScheme == .dark ?
+ Color(.systemGray5) : Color(.systemGray6))
+ .frame(width: 55, height: 55)
+
+ Image(systemName: "gamecontroller.fill")
+ .font(.system(size: 24))
+ .foregroundColor(.gray)
+ }
+ }
+
+ // Game Info
+ VStack(alignment: .leading, spacing: 4) {
+ Text(game.titleName)
+ .font(.system(size: 16, weight: .medium))
+ .foregroundColor(.primary)
+ .multilineTextAlignment(.leading)
+
+ HStack {
+ Text(game.developer)
+ .font(.system(size: 14))
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.leading)
+
+ if !game.version.isEmpty && game.version != "0" {
+ Text("•")
+ .foregroundColor(.secondary)
+
+ Text("v\(game.version)")
+ .font(.system(size: 14))
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+
+ Spacer()
+
+ VStack(alignment: .leading) {
+ // Compatibility badges
+ HStack {
+ if let gameReq = gameRequirements.first(where: { $0.game_id == game.titleId }) {
+ let totalMemory = ProcessInfo.processInfo.physicalMemory
+
+ HStack(spacing: 4) {
+ // Memory requirement badge
+ Text(gameReq.device_memory)
+ .font(.system(size: 11, weight: .medium))
+ .foregroundColor(.white)
+ .padding(.horizontal, 4)
+ .padding(.vertical, 4)
+ .background(
+ Capsule()
+ .fill(gameReq.memoryInt <= Int(String(format: "%.0f", Double(totalMemory) / 1_000_000_000)) ?? 0 ? Color.blue : Color.red)
+ )
+
+ // Compatibility badge
+ Text(gameReq.compatibility)
+ .font(.system(size: 11, weight: .medium))
+ .foregroundColor(.white)
+ .padding(.horizontal, 4)
+ .padding(.vertical, 4)
+ .background(
+ Capsule()
+ .fill(gameReq.color)
+ )
+ }
+ }
+
+ // Play button
+ Image(systemName: "play.circle.fill")
+ .font(.title3)
+ .foregroundColor(.blue)
+ }
+ }
+ }
+ .padding(.horizontal, 10)
+ .padding(.vertical, 4)
+ .frame(width: .infinity, height: .infinity)
+ }
+ .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")
+ }
+ }
+
+ 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 {
+ gametoDelete = game
+ showGameDeleteConfirmation.toggle()
+ } label: {
+ Label("Delete", systemImage: "trash")
+ .foregroundColor(.red)
+ }
+ }
+ }
+ .alert(isPresented: $showGameDeleteConfirmation) {
+ Alert(
+ title: Text("Are you sure you want to delete this game?"),
+ message: Text("Are you sure you want to delete \(gametoDelete?.titleName ?? "this game")?"),
+ primaryButton: .destructive(Text("Delete")) {
+ if let game = gametoDelete {
+ deleteGame(game: game)
+ }
+ },
+ secondaryButton: .cancel()
+ )
+ }
+ .listRowInsets(EdgeInsets())
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(colorScheme == .dark ? Color(.systemGray6) : Color(.systemGray6).opacity(0.5))
+ )
+ }
+ }
+
+ private func deleteGame(game: Game) {
+ let fileManager = FileManager.default
+ do {
+ try fileManager.removeItem(at: game.fileURL)
+ games.removeAll { $0.id == game.id }
+ } catch {
+ // print("Error deleting game: \(error)")
+ }
+ }
+}
+
+struct GameRequirements: Codable {
+ var game_id: String
+ var compatibility: String
+ var device_memory: String
+ var memoryInt: Int {
+ var devicemem = device_memory
+ devicemem.removeLast(2)
+ // print(devicemem)
+ return Int(devicemem) ?? 0
+ }
+
+ var color: Color {
+ switch compatibility {
+ case "Perfect":
+ return .green
+ case "Playable":
+ return .yellow
+ case "Menu":
+ return .orange
+ case "Boots":
+ return .red
+ case "Nothing":
+ return .black
+ default:
+ return .clear
+ }
+ }
+}
+
+func pullGameCompatibility(completion: @escaping (Result<[GameRequirements], Error>) -> Void) {
+ if let cachedData = GameCompatibiliryCache.shared.getCachedData() {
+ completion(.success(cachedData))
+ return
+ }
+
+ guard let url = URL(string: "https://melonx.net/api/game_entries") else {
+ completion(.failure(NSError(domain: "Invalid URL", code: 0, userInfo: nil)))
+ return
+ }
+
+ let task = URLSession.shared.dataTask(with: url) { data, response, error in
+ if let error = error {
+ completion(.failure(error))
+ return
+ }
+
+ guard let data = data else {
+ completion(.failure(NSError(domain: "No data", code: 0, userInfo: nil)))
+ return
+ }
+
+ do {
+ let decodedData = try JSONDecoder().decode([GameRequirements].self, from: data)
+ GameCompatibiliryCache.shared.setCachedData(decodedData)
+ completion(.success(decodedData))
+ } catch {
+ completion(.failure(error))
+ }
+ }
+
+ task.resume()
+}
+
+extension View {
+ func wow(_ colorScheme: ColorScheme) -> some View {
+ if #available(iOS 26.0, *) {
+ return self
+ .glassEffect(Glass.regular, in:
+ RoundedRectangle(cornerRadius: 12)
+ )
+ } 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()
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/JIT/JITPopover.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/JIT/JITPopover.swift
new file mode 100644
index 000000000..b779a0381
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/JIT/JITPopover.swift
@@ -0,0 +1,46 @@
+//
+// JITPopover.swift
+// MeloNX
+//
+// Created by Stossy11 on 05/03/2025.
+//
+
+import SwiftUI
+
+struct JITPopover: View {
+ var onJITEnabled: () -> Void
+ @Environment(\.presentationMode) var presentationMode
+
+ @State var isJIT: Bool = false
+ var body: some View {
+ VStack(spacing: 10) {
+ Image(systemName: "cpu.fill")
+ .font(.largeTitle)
+ .foregroundColor(.blue)
+
+ Text("Waiting for JIT")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ Text("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.")
+ .font(.footnote)
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.center)
+ .padding()
+ }
+ .padding()
+ .onAppear {
+ Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
+ isJIT = isJITEnabled()
+
+
+ if isJIT {
+ presentationMode.wrappedValue.dismiss()
+ onJITEnabled()
+
+ Ryujinx.shared.ryuIsJITEnabled()
+ }
+ }
+ }
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/Logging/Logs.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/Logging/Logs.swift
new file mode 100644
index 000000000..e3755aa6c
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/Logging/Logs.swift
@@ -0,0 +1,66 @@
+//
+// LogEntry.swift
+// MeloNX
+//
+// Created by Stossy11 on 09/02/2025.
+//
+
+import SwiftUI
+import Combine
+
+struct LogFileView: View {
+ @StateObject var logsModel = LogViewModel()
+ @State private var showingLogs = false
+
+ public var isfps: Bool
+
+ private let fileManager = FileManager.default
+ private let maxDisplayLines = 4
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(logsModel.logs.suffix(maxDisplayLines), id: \.self) { log in
+ Text(log)
+ .font(.caption)
+ .foregroundColor(.white)
+ .padding(4)
+ .background(Color.black.opacity(0.7))
+ .cornerRadius(4)
+ .transition(.opacity)
+ }
+ }
+ .padding()
+ }
+
+ private func stopLogFileWatching() {
+ showingLogs = false
+ }
+}
+
+
+class LogViewModel: ObservableObject {
+ @Published var logs: [String] = []
+ private var cancellables = Set()
+
+ init() {
+ _ = LogCapture.shared
+
+ NotificationCenter.default.publisher(for: .newLogCaptured)
+ .receive(on: RunLoop.main)
+ .sink { [weak self] _ in
+ self?.updateLogs()
+ }
+ .store(in: &cancellables)
+
+ updateLogs()
+ }
+
+ func updateLogs() {
+ logs = LogCapture.shared.capturedLogs
+ }
+
+ func clearLogs() {
+ LogCapture.shared.capturedLogs = []
+ updateLogs()
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/AppIcon/AppIconSwitcher.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/AppIcon/AppIconSwitcher.swift
new file mode 100644
index 000000000..aeea14df4
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/AppIcon/AppIconSwitcher.swift
@@ -0,0 +1,236 @@
+//
+// AppIconSwitcher.swift
+// MeloNX
+//
+// Created by Stossy11 on 02/06/2025.
+//
+
+import SwiftUI
+
+struct AppIcon: Identifiable, Equatable {
+ var id: String { creator }
+
+ var iconNames: [String: String]
+ var creator: String
+}
+
+struct AppIconSwitcherView: View {
+ @Environment(\.dismiss) private var dismiss
+ @State var appIcons: [AppIcon] = [
+ AppIcon(iconNames: ["Default": UIImage.appIcon(), "Dark Mode": "DarkMode", "Round": "RoundAppIcon"], creator: "CycloKid"),
+ AppIcon(iconNames: ["Pixel Default": "PixelAppIcon", "Pixel Round": "PixelRoundAppIcon"], creator: "Nobody"),
+ AppIcon(iconNames: ["\"UwU\"": "uwuAppIcon"], creator: "𝒰𝓃𝓀𝓃𝑜𝓌𝓃")
+
+ ]
+
+ @State var columns: [GridItem] = [
+ GridItem(.flexible(), spacing: 20),
+ GridItem(.flexible(), spacing: 20),
+ GridItem(.flexible(), spacing: 20)
+ ]
+ @State private var currentIconName: String? = nil
+ @State var refresh = 0
+
+ var body: some View {
+ NavigationView {
+ ZStack {
+ LinearGradient(
+ gradient: Gradient(colors: [
+ Color(.systemBackground).opacity(0.95),
+ Color(.systemGroupedBackground)
+ ]),
+ startPoint: .top,
+ endPoint: .bottom
+ )
+ .ignoresSafeArea()
+
+ ScrollView {
+ LazyVStack(spacing: 32) {
+ ForEach(appIcons.indices, id: \.self) { index in
+ let iconGroup = appIcons[index]
+
+ VStack(alignment: .leading, spacing: 20) {
+ HStack {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(iconGroup.creator)
+ .font(.title2)
+ .fontWeight(.bold)
+ .foregroundStyle(.primary)
+
+ Text("\(iconGroup.iconNames.count) icons")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ Spacer()
+ }
+ .padding(.horizontal, 24)
+
+ LazyVGrid(columns: columns, spacing: 20) {
+ ForEach(Array(iconGroup.iconNames.keys.sorted()), id: \.self) { key in
+ if let iconName = iconGroup.iconNames[key] {
+ Button {
+ selectIcon(iconName)
+ } label: {
+ ZStack {
+ AppIconView(app: (iconName, key))
+
+ if iconName == currentIconName ?? UIImage.appIcon() {
+ VStack {
+ HStack {
+ Spacer()
+ Image(systemName: "checkmark.circle.fill")
+ .font(.system(size: 24, weight: .bold))
+ .foregroundStyle(.white)
+ .background(
+ Circle()
+ .fill(
+ LinearGradient(
+ colors: [.blue, .purple],
+ startPoint: .topLeading,
+ endPoint: .bottomTrailing
+ )
+ )
+ .frame(width: 28, height: 28)
+ )
+ }
+ Spacer()
+ }
+ .frame(width: 80, height: 80)
+ .offset(x: 6, y: -6)
+ }
+ }
+ }
+ .buttonStyle(PlainButtonStyle())
+ .scaleEffect(isCurrentIcon(iconName) ? 0.95 : 1.0)
+ .animation(.spring(response: 0.3, dampingFraction: 0.7), value: isCurrentIcon(iconName))
+ }
+ }
+ }
+ .padding(.horizontal, 24)
+ }
+
+ // Stylized divider
+ if index < appIcons.count - 1 {
+ Rectangle()
+ .fill(
+ LinearGradient(
+ colors: [.clear, Color(.separator), .clear],
+ startPoint: .leading,
+ endPoint: .trailing
+ )
+ )
+ .frame(height: 1)
+ .padding(.horizontal, 40)
+ }
+ }
+ }
+ .padding(.vertical, 32)
+ }
+ }
+ .navigationTitle("Choose App Icon")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button("Done") {
+ dismiss()
+ }
+ .font(.system(size: 16, weight: .semibold))
+ .foregroundStyle(.blue)
+ }
+ }
+ }
+ .onAppear(perform: setupColumns)
+ .onAppear(perform: getCurrentIconName)
+ }
+
+ private func setupColumns() {
+ if #available(iOS 18.5, *) {
+ //
+ } else {
+ if checkforOld() {
+ if let value = appIcons[0].iconNames.removeValue(forKey: "Round") {
+ appIcons[0].iconNames["PomeloNX"] = value
+ }
+
+ if let value = appIcons[1].iconNames.removeValue(forKey: "Pixel Round") {
+ appIcons[1].iconNames["Pixel PomeloNX"] = value
+ }
+ }
+ }
+ }
+
+ private func getCurrentIconName() {
+ currentIconName = UIApplication.shared.alternateIconName ?? UIImage.appIcon()
+ }
+
+ private func isCurrentIcon(_ iconName: String) -> Bool {
+ let currentIcon = UIApplication.shared.alternateIconName ?? UIImage.appIcon()
+ return currentIcon == iconName
+ }
+
+ private func selectIcon(_ iconName: String) {
+ // Haptic feedback
+ let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
+ impactFeedback.impactOccurred()
+
+ if iconName == UIImage.appIcon() {
+ UIApplication.shared.setAlternateIconName(nil) { error in
+ if let error = error {
+ print("Error setting icon: \(error)")
+ } else {
+ DispatchQueue.main.async {
+ currentIconName = nil
+ refresh = Int.random(in: 0...100)
+ }
+ }
+ }
+ } else {
+ UIApplication.shared.setAlternateIconName(iconName) { error in
+ if let error = error {
+ print("Error setting icon: \(error)")
+ } else {
+ DispatchQueue.main.async {
+ currentIconName = iconName
+ refresh = Int.random(in: 0...100)
+ }
+ }
+ }
+ }
+ }
+}
+
+struct AppIconView: View {
+ let app: (String, String)
+
+ var body: some View {
+ VStack(spacing: 7) {
+ ZStack {
+ Image(uiImage: UIImage(named: app.0)!)
+ .resizable()
+ .cornerRadius(15)
+ .frame(width: 62, height: 62)
+ .shadow(color: .black.opacity(0.2), radius: 2, x: 0, y: 1)
+ }
+
+ Text(app.1)
+ .font(.system(size: 12, weight: .medium))
+ .foregroundColor(.white)
+ .multilineTextAlignment(.center)
+ .shadow(color: .black.opacity(0.2), radius: 2, x: 0, y: 1)
+ .frame(width: 100)
+ .lineLimit(1)
+ }
+ }
+}
+
+extension UIImage {
+ static func appIcon() -> String {
+ if let icons = Bundle.main.infoDictionary?["CFBundleIcons"] as? [String: Any],
+ let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any],
+ let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String],
+ let lastIcon = iconFiles.last {
+ return lastIcon
+ }
+ return "AppIcon"
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/Per-Game Settings/PerGameSettingsView.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/Per-Game Settings/PerGameSettingsView.swift
new file mode 100644
index 000000000..c53c8a99a
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/Per-Game Settings/PerGameSettingsView.swift
@@ -0,0 +1,707 @@
+//
+// PerGameSettingsView.swift
+// MeloNX
+//
+// Created by Stossy11 on 12/06/2025.
+//
+
+import SwiftUI
+
+protocol PerGameSettingsManaging: ObservableObject {
+ var config: [String: Ryujinx.Arguments] { get set }
+
+ func debouncedSave()
+ func saveSettings()
+ func loadSettings()
+
+ static func loadSettings() -> [String: Ryujinx.Arguments]?
+}
+
+
+
+class PerGameSettingsManager: PerGameSettingsManaging {
+ @Published var config: [String: Ryujinx.Arguments] {
+ didSet {
+ debouncedSave()
+ }
+ }
+
+ private var saveWorkItem: DispatchWorkItem?
+
+ public static var shared = PerGameSettingsManager()
+
+ private init() {
+ self.config = PerGameSettingsManager.loadSettings() ?? [:]
+ }
+
+ func debouncedSave() {
+ saveWorkItem?.cancel()
+
+ let workItem = DispatchWorkItem { [weak self] in
+ guard let self = self else { return }
+ self.saveSettings()
+ }
+
+ saveWorkItem = workItem
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: workItem)
+ }
+
+ func saveSettings() {
+ do {
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = .prettyPrinted
+ let data = try encoder.encode(config)
+
+ let fileURL = URL.documentsDirectory.appendingPathComponent("config-pergame.json")
+
+ try data.write(to: fileURL)
+ print("Settings saved successfully")
+ } catch {
+ print("Failed to save settings: \(error)")
+ }
+ }
+
+ static func loadSettings() -> [String: Ryujinx.Arguments]? {
+ do {
+ let fileURL = URL.documentsDirectory.appendingPathComponent("config-pergame.json")
+
+ guard FileManager.default.fileExists(atPath: fileURL.path) else {
+ print("Config file does not exist, creating new config")
+ return nil
+ }
+
+ let data = try Data(contentsOf: fileURL)
+
+ let decoder = JSONDecoder()
+ let configs = try decoder.decode([String: Ryujinx.Arguments].self, from: data)
+ return configs
+ } catch {
+ print("Failed to load settings: \(error)")
+ return nil
+ }
+ }
+
+ func loadSettings() {
+ self.config = PerGameSettingsManager.loadSettings() ?? [:]
+ }
+}
+
+
+struct PerGameSettingsView: View {
+
+ @StateObject private var settingsManager: PerGameSettingsManager
+
+ var titleId: String
+
+ init(titleId: String, manager: any PerGameSettingsManaging = PerGameSettingsManager.shared) {
+ self._settingsManager = StateObject(wrappedValue: manager as! PerGameSettingsManager)
+ self.titleId = titleId
+ }
+
+
+ private var config: Binding {
+ return Binding {
+ return settingsManager.config[titleId] ?? Ryujinx.Arguments()
+ } set: { newValue in
+ settingsManager.config[titleId] = newValue
+ settingsManager.debouncedSave()
+ }
+ }
+
+ var memoryManagerModes = [
+ ("HostMapped", "Host (fast)"),
+ ("HostMappedUnsafe", "Host Unchecked (fast, unstable / unsafe)"),
+ ("SoftwarePageTable", "Software (slow)"),
+ ]
+
+
+ let totalMemory = ProcessInfo.processInfo.physicalMemory
+
+ @State private var showResolutionInfo = false
+ @State private var showAnisotropicInfo = false
+ @State private var showControllerInfo = false
+ @State private var showAppIconSwitcher = false
+ @State private var searchText = ""
+ @StateObject var ryujinx = Ryujinx.shared
+ @Environment(\.dismiss) var dismiss
+ @Environment(\.colorScheme) var colorScheme
+ @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
+ @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
+
+ @State private var selectedCategory: PerSettingsCategory = .graphics
+
+ @StateObject var metalHudEnabler = MTLHud.shared
+
+ var filteredMemoryModes: [(String, String)] {
+ guard !searchText.isEmpty else { return memoryManagerModes }
+ return memoryManagerModes.filter { $0.1.localizedCaseInsensitiveContains(searchText) }
+ }
+
+ var appVersion: String {
+ guard let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
+ return "Unknown"
+ }
+ return version
+ }
+
+ @FocusState private var isArgumentsKeyboardVisible: Bool
+
+
+ @State private var selectedView = "Data Management"
+ @State private var sidebar = true
+
+ enum PerSettingsCategory: String, CaseIterable, Identifiable {
+ case graphics = "Graphics"
+ case system = "System"
+ case advanced = "Advanced"
+
+ var id: String { self.rawValue }
+
+ var icon: String {
+ switch self {
+ case .graphics: return "paintbrush.fill"
+ case .system: return "gearshape.fill"
+ case .advanced: return "terminal.fill"
+ }
+ }
+ }
+
+ var body: some View {
+ iOSNav {
+ ZStack {
+ Color(UIColor.systemBackground)
+ .ignoresSafeArea()
+
+ VStack(spacing: 0) {
+ 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
+ }
+ }
+ }
+ .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()
+ }
+ }
+ .navigationTitle("Settings")
+ .navigationBarTitleDisplayMode(.large)
+ .toolbar {
+ ToolbarItem(placement: .confirmationAction) {
+ Button("Done") {
+ settingsManager.debouncedSave()
+ dismiss()
+ }
+ }
+
+ ToolbarItem(placement: .cancellationAction) {
+ Button("Reset") {
+ dismiss()
+ settingsManager.config[titleId] = nil
+ settingsManager.saveSettings()
+ }
+ }
+ }
+ // .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic))
+ .onAppear {
+
+ // if let configs = SettingsManager.loadSettings() {
+ // settingsManager.loadSettings()
+ // } else {
+ // settingsManager.saveSettings()
+ //}
+
+ print(titleId)
+
+ if settingsManager.config[titleId] == nil {
+ settingsManager.config[titleId] = Ryujinx.Arguments()
+ settingsManager.debouncedSave()
+ }
+ }
+ }
+ }
+
+ // MARK: - Graphics Settings
+
+ private var graphicsSettings: some View {
+ SettingsSection(title: "Graphics & Performance") {
+ // Resolution scale card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ labelWithIcon("Resolution Scale", iconName: "magnifyingglass")
+ .font(.headline)
+ Spacer()
+ Button {
+ showResolutionInfo.toggle()
+ } label: {
+ Image(systemName: "info.circle")
+ .foregroundColor(.secondary)
+ }
+ .buttonStyle(.plain)
+ .alert(isPresented: $showResolutionInfo) {
+ Alert(
+ title: Text("Resolution Scale"),
+ message: Text("Adjust the internal rendering resolution. Higher values improve visuals but may reduce performance."),
+ dismissButton: .default(Text("OK"))
+ )
+ }
+ }
+
+ VStack(spacing: 8) {
+ Slider(value: config.resscale, in: 0.1...3.0, step: 0.05)
+
+ HStack {
+ Text("0.1x")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+
+ Spacer()
+
+ Text("\(config.resscale.wrappedValue, specifier: "%.2f")x")
+ .font(.headline)
+ .foregroundColor(.blue)
+
+ Spacer()
+
+ Text("3.0x")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+ }
+
+ // Anisotropic filtering card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ labelWithIcon("Max Anisotropic Filtering", iconName: "magnifyingglass")
+ .font(.headline)
+ Spacer()
+ Button {
+ showAnisotropicInfo.toggle()
+ } label: {
+ Image(systemName: "info.circle")
+ .foregroundColor(.secondary)
+ }
+ .buttonStyle(.plain)
+ .alert(isPresented: $showAnisotropicInfo) {
+ Alert(
+ title: Text("Max Anisotropic Filtering"),
+ message: Text("Adjust the internal Anisotropic filtering. Higher values improve texture quality at angles but may reduce performance. Default at 0 lets game decide."),
+ dismissButton: .default(Text("OK"))
+ )
+ }
+ }
+
+ VStack(spacing: 8) {
+ Slider(value: config.maxAnisotropy, in: 0...16.0, step: 0.1)
+
+ HStack {
+ Text("Off")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+
+ Spacer()
+
+ Text("\(config.maxAnisotropy.wrappedValue, specifier: "%.1f")x")
+ .font(.headline)
+ .foregroundColor(.blue)
+
+ Spacer()
+
+ Text("16x")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+ }
+
+ // Toggle options card
+ SettingsCard {
+ VStack(spacing: 4) {
+ PerSettingsToggle(isOn: config.disableShaderCache, icon: "memorychip", label: "Shader Cache")
+
+ Divider()
+
+ PerSettingsToggle(isOn: config.disablevsync, icon: "arrow.triangle.2.circlepath", label: "Disable VSync")
+
+ Divider()
+
+ PerSettingsToggle(isOn: config.enableTextureRecompression, icon: "rectangle.compress.vertical", label: "Texture Recompression")
+
+ Divider()
+
+ PerSettingsToggle(isOn: config.disableDockedMode, icon: "dock.rectangle", label: "Docked Mode")
+
+ Divider()
+
+ PerSettingsToggle(isOn: config.macroHLE, icon: "gearshape", label: "Macro HLE")
+ }
+ }
+
+ // Aspect ratio card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ labelWithIcon("Aspect Ratio", iconName: "rectangle.expand.vertical")
+ .font(.headline)
+
+ if (horizontalSizeClass == .regular && verticalSizeClass == .regular) || (horizontalSizeClass == .regular && verticalSizeClass == .compact) {
+ Picker(selection: config.aspectRatio) {
+ ForEach(AspectRatio.allCases, id: \.self) { ratio in
+ Text(ratio.displayName).tag(ratio)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.segmented)
+ } else {
+ Picker(selection: config.aspectRatio) {
+ ForEach(AspectRatio.allCases, id: \.self) { ratio in
+ Text(ratio.displayName).tag(ratio)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.menu)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.vertical, 4)
+ }
+ }
+ }
+ }
+ }
+
+
+ // MARK: - System Settings
+
+ private var systemSettings: some View {
+ SettingsSection(title: "System Configuration") {
+ // Language and region card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 16) {
+ VStack(alignment: .leading, spacing: 8) {
+ labelWithIcon("System Language", iconName: "character.bubble")
+ .font(.headline)
+
+ Picker(selection: config.language) {
+ ForEach(SystemLanguage.allCases, id: \.self) { language in
+ Text(language.displayName).tag(language)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.menu)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.vertical, 4)
+ }
+
+ Divider()
+
+ VStack(alignment: .leading, spacing: 8) {
+ labelWithIcon("Region", iconName: "globe")
+ .font(.headline)
+
+ Picker(selection: config.regioncode) {
+ ForEach(SystemRegionCode.allCases, id: \.self) { region in
+ Text(region.displayName).tag(region)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.menu)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.vertical, 4)
+ }
+ }
+ }
+
+ // CPU options card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 16) {
+ Text("CPU Configuration")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Memory Manager Mode")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+
+ Picker(selection: config.memoryManagerMode) {
+ ForEach(filteredMemoryModes, id: \.0) { key, displayName in
+ Text(displayName).tag(key)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.segmented)
+ }
+
+ Divider()
+
+ PerSettingsToggle(isOn: config.disablePTC, icon: "cpu", label: "Disable PTC")
+
+ if let gpuInfo = getGPUInfo(), gpuInfo.hasPrefix("Apple M") {
+ Divider()
+
+ if #available(iOS 16.4, *) {
+ PerSettingsToggle(isOn: .constant(false), icon: "bolt", label: "Hypervisor")
+ .disabled(true)
+ } else if checkAppEntitlement("com.apple.private.hypervisor") {
+ PerSettingsToggle(isOn: config.hypervisor, icon: "bolt", label: "Hypervisor")
+ }
+ }
+ }
+ }
+
+ // Controller options card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 16) {
+ Text("Controller Configuration")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ PerSettingsToggle(isOn: config.handHeldController, icon: "formfitting.gamecontroller", label: "Player 1 to Handheld")
+
+ }
+ }
+ }
+ }
+
+ // MARK: - Advanced Settings
+
+ private var advancedSettings: some View {
+ SettingsSection(title: "Advanced Options") {
+ // Debug options card
+ SettingsCard {
+ VStack(spacing: 4) {
+ PerSettingsToggle(isOn: config.debuglogs, icon: "exclamationmark.bubble", label: "Debug Logs")
+
+ Divider()
+
+ PerSettingsToggle(isOn: config.tracelogs, icon: "waveform.path", label: "Trace Logs")
+ }
+ }
+
+ // Advanced toggles card
+ SettingsCard {
+ VStack(spacing: 4) {
+
+ PerSettingsToggle(isOn: config.dfsIntegrityChecks, icon: "checkmark.shield", label: "Disable FS Integrity Checks")
+ .accentColor(.red)
+
+ Divider()
+
+ PerSettingsToggle(isOn: config.expandRam, icon: "exclamationmark.bubble", label: "Expand Guest RAM")
+ .accentColor(.red)
+ .disabled(totalMemory < 5723)
+
+ Divider()
+
+ PerSettingsToggle(isOn: config.ignoreMissingServices, icon: "waveform.path", label: "Ignore Missing Services")
+ .accentColor(.red)
+ }
+ }
+
+ // Additional args card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ Text("Additional Arguments")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ let binding = Binding(
+ get: {
+ config.additionalArgs.wrappedValue.joined(separator: ", ")
+ },
+ set: { newValue in
+ let args = newValue
+ .split(separator: ",")
+ .map { $0.trimmingCharacters(in: .whitespaces) }
+ config.additionalArgs.wrappedValue = args
+ }
+ )
+
+
+ if #available(iOS 15.0, *) {
+ TextField("Separate arguments with commas", text: binding)
+ .font(.system(.body, design: .monospaced))
+ .textFieldStyle(.roundedBorder)
+ .textInputAutocapitalization(.none)
+ .disableAutocorrection(true)
+ .padding(.vertical, 4)
+ .toolbar {
+ ToolbarItem(placement: .keyboard) {
+ Button("Dismiss") {
+ isArgumentsKeyboardVisible = false
+ }
+ }
+ }
+ .focused($isArgumentsKeyboardVisible)
+ } else {
+ TextField("Separate arguments with commas", text: binding)
+ .font(.system(.body, design: .monospaced))
+ .textFieldStyle(.roundedBorder)
+ .disableAutocorrection(true)
+ .padding(.vertical, 4)
+ }
+ }
+ }
+
+ // Page size info card
+ SettingsCard {
+ HStack {
+ labelWithIcon("Page Size", iconName: "textformat.size")
+ Spacer()
+ Text("\(String(Int(getpagesize())))")
+ .font(.system(.body, design: .monospaced))
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+ }
+
+ // MARK: - Miscellaneous Settings
+
+ private var miscSettings: some View {
+ SettingsSection(title: "Miscellaneous Options") {
+ SettingsCard {
+ VStack(spacing: 4) {
+ PerSettingsToggle(isOn: config.handHeldController, icon: "formfitting.gamecontroller", label: "Player 1 to Handheld")
+ }
+ }
+ }
+ }
+
+ // MARK: - Helper Functions
+
+
+ func getGPUInfo() -> String? {
+ let device = MTLCreateSystemDefaultDevice()
+ return device?.name
+ }
+
+ @ViewBuilder
+ private func labelWithIcon(_ text: String, iconName: String, flipimage: Bool? = nil) -> some View {
+ HStack(spacing: 8) {
+ if iconName.hasSuffix(".svg") {
+ if let flipimage, flipimage {
+ SVGView(svgName: iconName, color: .blue)
+ // .symbolRenderingMode(.hierarchical)
+ .frame(width: 20, height: 20)
+ .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
+ } else {
+ SVGView(svgName: iconName, color: .blue)
+ // .symbolRenderingMode(.hierarchical)
+ .frame(width: 20, height: 20)
+ }
+ } else if !iconName.isEmpty {
+ Image(systemName: iconName)
+ // .symbolRenderingMode(.hierarchical)
+ .foregroundColor(.blue)
+ }
+ Text(text)
+ }
+ .font(.body)
+ }
+}
+
+
+// MARK: - Supporting Views
+
+// PerSettingsToggle(isOn: config.handHeldController, icon: "formfitting.gamecontroller", label: "Player 1 to Handheld")
+
+struct PerSettingsCard: View {
+ @Environment(\.colorScheme) var colorScheme
+ @AppStorage("oldSettingsUI") var oldSettingsUI = false
+ let content: Content
+
+ init(@ViewBuilder content: () -> Content) {
+ self.content = content()
+ }
+
+ var body: some View {
+ content
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(colorScheme == .dark ? Color(.systemGray6) : Color.white)
+ .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
+ )
+ .padding(.horizontal)
+ }
+}
+
+struct PerSettingsToggle: View {
+ @Binding var isOn: Bool
+ let icon: String
+ let label: String
+ var disabled: Bool = false
+ @AppStorage("toggleGreen") var toggleGreen: Bool = false
+ @AppStorage("oldSettingsUI") var oldSettingsUI = false
+
+ var body: some View {
+ Toggle(isOn: $isOn) {
+ HStack(spacing: 8) {
+ if icon.hasSuffix(".svg") {
+ SVGView(svgName: icon, color: .blue)
+ .frame(width: 20, height: 20)
+ } else {
+ Image(systemName: icon)
+ // .symbolRenderingMode(.hierarchical)
+ .foregroundColor(.blue)
+ }
+
+ Text(label)
+ .font(.body)
+ }
+ }
+ .toggleStyle(SwitchToggleStyle(tint: .blue))
+ .disabled(disabled)
+ .padding(.vertical, 6)
+ }
+
+ func disabled(_ disabled: Bool) -> PerSettingsToggle {
+ var view = self
+ view.disabled = disabled
+ return view
+ }
+
+ func accentColor(_ color: Color) -> PerSettingsToggle {
+ var view = self
+ return view
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/SettingsView.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/SettingsView.swift
new file mode 100644
index 000000000..4205d284f
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/SettingsView.swift
@@ -0,0 +1,1669 @@
+//
+// SettingsView.swift
+// MeloNX
+//
+// Created by Stossy11 on 25/11/2024.
+//
+
+import SwiftUI
+import SwiftSVG
+import UIKit
+
+
+class SplitViewController: UISplitViewController {
+ private let sidebarViewController: UIViewController
+ private let contentViewController: UIViewController
+
+ init(sidebarViewController: UIViewController, contentViewController: UIViewController) {
+ self.sidebarViewController = sidebarViewController
+ self.contentViewController = contentViewController
+ super.init(style: .doubleColumn)
+
+ self.preferredDisplayMode = .oneBesideSecondary
+ self.preferredSplitBehavior = .tile
+ self.presentsWithGesture = true
+
+ self.setViewController(sidebarViewController, for: .primary)
+ self.setViewController(contentViewController, for: .secondary)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ self.primaryBackgroundStyle = .sidebar
+
+ let displayModeButtonItem = self.displayModeButtonItem
+ contentViewController.navigationItem.leftBarButtonItem = displayModeButtonItem
+ }
+
+ func showSidebar() {
+ self.preferredDisplayMode = .oneBesideSecondary
+ }
+
+ func hideSidebar() {
+ self.preferredDisplayMode = .secondaryOnly
+ }
+
+ func toggleSidebar() {
+ if self.displayMode == .oneBesideSecondary {
+ self.preferredDisplayMode = .secondaryOnly
+ } else {
+ self.preferredDisplayMode = .oneBesideSecondary
+ }
+ }
+}
+
+struct SidebarView: View {
+ var sidebar: () -> AnyView
+ var content: () -> Content
+ @Binding var showSidebar: Bool
+
+ init(sidebar: @escaping () -> AnyView, content: @escaping () -> Content, showSidebar: Binding) {
+ self.sidebar = sidebar
+ self.content = content
+ self._showSidebar = showSidebar
+ }
+
+ var body: some View {
+ SidebarViewRepresentable(
+ sidebar: sidebar(),
+ content: content(),
+ showSidebar: $showSidebar
+ )
+ }
+}
+
+struct SidebarViewRepresentable: UIViewControllerRepresentable {
+ var sidebar: Sidebar
+ var content: Content
+ @Binding var showSidebar: Bool
+
+ func makeUIViewController(context: Context) -> SplitViewController {
+ let sidebarVC = UIHostingController(rootView: sidebar)
+ let contentVC = UINavigationController(rootViewController: UIHostingController(rootView: content))
+
+ let splitVC = SplitViewController(sidebarViewController: sidebarVC, contentViewController: contentVC)
+ splitVC.setOverrideTraitCollection(
+ UITraitCollection(horizontalSizeClass: .regular),
+ forChild: splitVC
+ )
+ return splitVC
+ }
+
+ func updateUIViewController(_ uiViewController: SplitViewController, context: Context) {
+ if let sidebarVC = uiViewController.viewController(for: .primary) as? UIHostingController {
+ sidebarVC.rootView = sidebar
+ }
+ if let navController = uiViewController.viewController(for: .secondary) as? UINavigationController,
+ let contentVC = navController.topViewController as? UIHostingController {
+ contentVC.rootView = content
+ }
+
+ if showSidebar {
+ uiViewController.showSidebar()
+ } else {
+ uiViewController.hideSidebar()
+ }
+ }
+
+ static func dismantleUIViewController(_ uiViewController: SplitViewController, coordinator: Coordinator) {
+ }
+}
+
+class SettingsManager: ObservableObject {
+ @Published var config: Ryujinx.Arguments {
+ didSet {
+ debouncedSave()
+ }
+ }
+
+ private var saveWorkItem: DispatchWorkItem?;
+
+ public static var shared = SettingsManager()
+
+ private init() {
+ self.config = SettingsManager.loadSettings() ?? Ryujinx.Arguments()
+ }
+
+ func debouncedSave() {
+ saveWorkItem?.cancel()
+
+ let workItem = DispatchWorkItem { [weak self] in
+ guard let self = self else { return }
+ self.saveSettings()
+ }
+
+ saveWorkItem = workItem
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: workItem)
+ }
+
+ func saveSettings() {
+ do {
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = .prettyPrinted
+ let data = try encoder.encode(config)
+
+ let fileURL = URL.documentsDirectory.appendingPathComponent("config.json")
+
+ try data.write(to: fileURL)
+ print("Settings saved successfully")
+ } catch {
+ print("Failed to save settings: \(error)")
+ }
+ }
+
+ static func loadSettings() -> Ryujinx.Arguments? {
+ do {
+ let fileURL = URL.documentsDirectory.appendingPathComponent("config.json")
+
+ guard FileManager.default.fileExists(atPath: fileURL.path) else {
+ print("Config file does not exist, creating new config")
+ return nil
+ }
+
+ let data = try Data(contentsOf: fileURL)
+
+ let decoder = JSONDecoder()
+ let configs = try decoder.decode(Ryujinx.Arguments.self, from: data)
+ return configs
+ } catch {
+ print("Failed to load settings: \(error)")
+ return nil
+ }
+ }
+
+ func loadSettings() {
+ do {
+ let fileURL = URL.documentsDirectory.appendingPathComponent("config.json")
+
+ guard FileManager.default.fileExists(atPath: fileURL.path) else {
+ print("Config file does not exist, creating new config")
+ saveSettings()
+ return
+ }
+
+ let data = try Data(contentsOf: fileURL)
+
+ let decoder = JSONDecoder()
+ let configs = try decoder.decode(Ryujinx.Arguments.self, from: data)
+
+ self.config = configs
+ } catch {
+ print("Failed to load settings: \(error)")
+ }
+ }
+}
+
+
+
+struct SettingsViewNew: View {
+ @StateObject private var settingsManager = SettingsManager.shared
+
+ private var config: Binding {
+ $settingsManager.config
+ }
+
+ @Binding var MoltenVKSettings: [MoltenVKSettings]
+
+ @Binding var controllersList: [Controller]
+ @Binding var currentControllers: [Controller]
+
+ @Binding var onscreencontroller: Controller
+ @AppStorage("useTrollStore") var useTrollStore: Bool = false
+
+ @AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
+ @AppStorage("stikJIT") var stikJIT: Bool = false
+
+ @AppStorage("ignoreJIT") var ignoreJIT: Bool = false
+
+ var memoryManagerModes = [
+ ("HostMapped", "Host (fast)"),
+ ("HostMappedUnsafe", "Host Unchecked (fast, unstable / unsafe)"),
+ ("SoftwarePageTable", "Software (slow)"),
+ ]
+
+ @AppStorage("RyuDemoControls") var ryuDemo: Bool = false
+
+ @AppStorage("showScreenShotButton") var ssb: Bool = false
+
+ @AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = false
+ @AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = false
+
+ @AppStorage("performacehud") var performacehud: Bool = false
+
+ @AppStorage("swapBandA") var swapBandA: Bool = false
+
+ @AppStorage("oldWindowCode") var windowCode: Bool = false
+
+ @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
+
+ @AppStorage("On-ScreenControllerOpacity") var controllerOpacity: Double = 1.0
+
+ @AppStorage("hasbeenfinished") var finishedStorage: Bool = false
+
+ @AppStorage("showlogsloading") var showlogsloading: Bool = true
+
+ @AppStorage("showlogsgame") var showlogsgame: Bool = false
+
+ @AppStorage("toggleGreen") var toggleGreen: Bool = false
+
+ @AppStorage("stick-button") var stickButton = false
+ @AppStorage("waitForVPN") var waitForVPN = false
+
+ @AppStorage("HideButtons") var hideButtonsJoy = false
+
+ @AppStorage("checkForUpdate") var checkForUpdate: Bool = true
+
+ @AppStorage("disableTouch") var disableTouch = false
+
+ @AppStorage("disableTouch") var blackScreen = false
+
+ @AppStorage("location-enabled") var locationenabled: Bool = false
+
+ @AppStorage("runOnMainThread") var runOnMainThread = false
+
+ @AppStorage("oldSettingsUI") var oldSettingsUI = false
+
+ @AppCodableStorage("toggleButtons") var toggleButtons = ToggleButtonsState()
+
+ let totalMemory = ProcessInfo.processInfo.physicalMemory
+
+ @AppStorage("lockInApp") var restartApp = false
+
+ @State private var showResolutionInfo = false
+ @State private var showAnisotropicInfo = false
+ @State private var showControllerInfo = false
+ @State private var showAppIconSwitcher = false
+ @State private var searchText = ""
+ @AppStorage("portal") var gamepo = false
+ @StateObject var ryujinx = Ryujinx.shared
+ @Environment(\.colorScheme) var colorScheme
+ @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
+ @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
+
+ @State private var selectedCategory: SettingsCategory = .graphics
+
+ @StateObject var metalHudEnabler = MTLHud.shared
+
+ var filteredMemoryModes: [(String, String)] {
+ guard !searchText.isEmpty else { return memoryManagerModes }
+ return memoryManagerModes.filter { $0.1.localizedCaseInsensitiveContains(searchText) }
+ }
+
+ var appVersion: String {
+ guard let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
+ return "Unknown"
+ }
+ return version
+ }
+
+ @FocusState private var isArgumentsKeyboardVisible: Bool
+
+
+ @State private var selectedView = "Data Management"
+ @State private var sidebar = true
+
+ enum SettingsCategory: String, CaseIterable, Identifiable {
+ case graphics = "Graphics"
+ case input = "Input"
+ case system = "System"
+ case misc = "Misc"
+ case advanced = "Advanced"
+
+ var id: String { self.rawValue }
+
+ var icon: String {
+ switch self {
+ case .graphics: return "paintbrush.fill"
+ case .input: return "gamecontroller.fill"
+ case .system: return "gearshape.fill"
+ case .misc: return "ellipsis.circle.fill"
+ case .advanced: return "terminal.fill"
+ }
+ }
+ }
+
+ var body: some View {
+ if UIDevice.current.userInterfaceIdiom == .phone {
+ iOSSettings
+ } else if !oldSettingsUI {
+ iPadOSSettings
+ .ignoresSafeArea()
+ .edgesIgnoringSafeArea(.all)
+ } else {
+ iOSSettings
+ }
+ }
+
+ var iPadOSSettings: some View {
+ VStack {
+ SidebarView(
+ sidebar: {
+ AnyView(
+ ScrollView(.vertical) {
+ VStack {
+ VStack(spacing: 16) {
+ HStack {
+ Circle()
+ .fill(ryujinx.jitenabled ? Color.green : Color.red)
+ .frame(width: 12, height: 12)
+
+ Text(ryujinx.jitenabled ? "JIT Enabled" : "JIT Not Acquired")
+ .font(.subheadline.weight(.medium))
+ .foregroundColor(ryujinx.jitenabled ? .green : .red)
+
+ Spacer()
+
+ let memoryText = ProcessInfo.processInfo.isiOSAppOnMac
+ ? String(format: "%.0f GB", Double(totalMemory) / (1024 * 1024 * 1024))
+ : String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000)
+
+ Text("\(memoryText) RAM")
+ .font(.subheadline.weight(.medium))
+ .foregroundColor(.secondary)
+
+ }
+
+ InfoCard(
+ title: "Device",
+ value: UIDevice.modelName,
+ icon: deviceIcon,
+ color: .blue
+ )
+
+ InfoCard(
+ title: "System",
+ value: "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)",
+ icon: "applelogo",
+ color: .gray
+ )
+
+ InfoCard(
+ title: "Increased Memory Limit",
+ value: checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled",
+ icon: "memorychip.fill",
+ color: .orange
+ )
+ }
+ .padding()
+
+ Divider()
+
+ ForEach(SettingsCategory.allCases, id: \.id) { key in
+ HStack {
+ Rectangle()
+ .frame(width: 2.5, height: 35)
+ .foregroundStyle(selectedCategory == key ? Color.accentColor : Color.clear)
+ Text(key.rawValue) // Fix here
+ Spacer()
+ }
+ .foregroundStyle(selectedCategory == key ? Color.accentColor : Color.primary)
+ .padding(5)
+ .background(
+ Color(uiColor: .secondarySystemBackground).opacity(selectedCategory == key ? 1 : 0)
+ )
+ .background(
+ Rectangle()
+ .stroke(selectedCategory == key ? .teal : .clear, lineWidth: 2.5)
+ )
+ .contentShape(Rectangle())
+ .onTapGesture {
+ withAnimation(.smooth) {
+ selectedCategory = key // Uncommented and fixed
+ }
+ }
+ }
+ }
+ .padding()
+ }
+ )
+ },
+ content: {
+ ScrollView {
+ switch selectedCategory {
+ case .graphics:
+ graphicsSettings
+ case .input:
+ inputSettings
+ case .system:
+ systemSettings
+ case .advanced:
+ advancedSettings
+ case .misc:
+ miscSettings
+ }
+ }
+ },
+ showSidebar: $sidebar
+ )
+ .onAppear {
+ mVKPreFillBuffer = false
+
+
+ if let configs = SettingsManager.loadSettings() {
+ settingsManager.loadSettings()
+ } else {
+ settingsManager.saveSettings()
+ }
+ }
+ }
+ }
+
+
+ var iOSSettings: some View {
+ iOSNav {
+ ZStack {
+ // Background color
+ Color(UIColor.systemBackground)
+ .ignoresSafeArea()
+
+ 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(.vertical, 8)
+ }
+
+ Divider()
+
+ // 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()
+ }
+ }
+ .navigationTitle("Settings")
+ .navigationBarTitleDisplayMode(.large)
+ // .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic))
+ .onAppear {
+ mVKPreFillBuffer = false
+
+ if let configs = SettingsManager.loadSettings() {
+ settingsManager.loadSettings()
+ } else {
+ settingsManager.saveSettings()
+ }
+ }
+ }
+ }
+
+ // MARK: - Device Info Card
+
+ private var deviceInfoCard: some View {
+ VStack(spacing: 16) {
+ // JIT Status indicator
+ HStack {
+ Circle()
+ .fill(ryujinx.jitenabled ? Color.green : Color.red)
+ .frame(width: 12, height: 12)
+
+ Text(ryujinx.jitenabled ? "JIT Enabled" : "JIT Not Acquired")
+ .font(.subheadline.weight(.medium))
+ .foregroundColor(ryujinx.jitenabled ? .green : .red)
+
+ Spacer()
+
+ let memoryText = ProcessInfo.processInfo.isiOSAppOnMac
+ ? String(format: "%.0f GB", Double(totalMemory) / (1024 * 1024 * 1024))
+ : String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000)
+
+ Text("\(memoryText) RAM")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+
+ Text("·")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+
+ Text("Version \(appVersion)")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ }
+
+ // Device cards
+ if (horizontalSizeClass == .regular && verticalSizeClass == .regular) || (horizontalSizeClass == .regular && verticalSizeClass == .compact) {
+ HStack(spacing: 16) {
+ InfoCard(
+ title: "Device",
+ value: UIDevice.modelName,
+ icon: deviceIcon,
+ color: .blue
+ )
+
+ InfoCard(
+ title: "System",
+ value: "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)",
+ icon: "applelogo",
+ color: .gray
+ )
+
+ InfoCard(
+ title: "Increased Memory Limit",
+ value: checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled",
+ icon: "memorychip.fill",
+ color: .orange
+ )
+ }
+ } else {
+ VStack(spacing: 16) {
+ InfoCard(
+ title: "Device",
+ value: UIDevice.modelName,
+ icon: deviceIcon,
+ color: .blue
+ )
+
+ InfoCard(
+ title: "System",
+ value: "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)",
+ icon: "applelogo",
+ color: .gray
+ )
+
+ InfoCard(
+ title: "Increased Memory Limit",
+ value: checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled",
+ icon: "memorychip.fill",
+ color: .orange
+ )
+ }
+ }
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 16)
+ .fill(colorScheme == .dark ? Color(.systemGray6) : Color.white)
+ .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
+ )
+ .onAppear {
+ ryujinx.ryuIsJITEnabled()
+ }
+ }
+
+ private var deviceIcon: String {
+ let model = UIDevice.modelName
+ if model.contains("iPad") {
+ return "ipad"
+ } else if model.contains("iPhone") {
+ return "iphone"
+ } else {
+ return "desktopcomputer"
+ }
+ }
+
+ // MARK: - Graphics Settings
+
+ private var graphicsSettings: some View {
+ SettingsSection(title: "Graphics & Performance") {
+ // Resolution scale card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ labelWithIcon("Resolution Scale", iconName: "magnifyingglass")
+ .font(.headline)
+ Spacer()
+ Button {
+ showResolutionInfo.toggle()
+ } label: {
+ Image(systemName: "info.circle")
+ .foregroundColor(.secondary)
+ }
+ .buttonStyle(.plain)
+ .alert(isPresented: $showResolutionInfo) {
+ Alert(
+ title: Text("Resolution Scale"),
+ message: Text("Adjust the internal rendering resolution. Higher values improve visuals but may reduce performance."),
+ dismissButton: .default(Text("OK"))
+ )
+ }
+ }
+
+ VStack(spacing: 8) {
+ Slider(value: config.resscale, in: 0.1...3.0, step: 0.05)
+
+ HStack {
+ Text("0.1x")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+
+ Spacer()
+
+ Text("\(settingsManager.config.resscale, specifier: "%.2f")x")
+ .font(.headline)
+ .foregroundColor(.blue)
+
+ Spacer()
+
+ Text("3.0x")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+ }
+
+ // Anisotropic filtering card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ labelWithIcon("Max Anisotropic Filtering", iconName: "magnifyingglass")
+ .font(.headline)
+ Spacer()
+ Button {
+ showAnisotropicInfo.toggle()
+ } label: {
+ Image(systemName: "info.circle")
+ .foregroundColor(.secondary)
+ }
+ .buttonStyle(.plain)
+ .alert(isPresented: $showAnisotropicInfo) {
+ Alert(
+ title: Text("Max Anisotropic Filtering"),
+ message: Text("Adjust the internal Anisotropic filtering. Higher values improve texture quality at angles but may reduce performance. Default at 0 lets game decide."),
+ dismissButton: .default(Text("OK"))
+ )
+ }
+ }
+
+ VStack(spacing: 8) {
+ Slider(value: config.maxAnisotropy, in: 0...16.0, step: 0.1)
+
+ HStack {
+ Text("Off")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+
+ Spacer()
+
+ Text("\(settingsManager.config.maxAnisotropy, specifier: "%.1f")x")
+ .font(.headline)
+ .foregroundColor(.blue)
+
+ Spacer()
+
+ Text("16x")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+ }
+
+ // Toggle options card
+ SettingsCard {
+ VStack(spacing: 4) {
+ SettingsToggle(isOn: config.disableShaderCache, icon: "memorychip", label: "Shader Cache")
+
+ Divider()
+
+ SettingsToggle(isOn: config.disablevsync, icon: "arrow.triangle.2.circlepath", label: "Disable VSync")
+
+ Divider()
+
+ SettingsToggle(isOn: config.enableTextureRecompression, icon: "rectangle.compress.vertical", label: "Texture Recompression")
+
+ Divider()
+
+ SettingsToggle(isOn: config.disableDockedMode, icon: "dock.rectangle", label: "Docked Mode")
+
+ Divider()
+
+ SettingsToggle(isOn: config.macroHLE, icon: "gearshape", label: "Macro HLE")
+
+ Divider()
+
+ SettingsToggle(isOn: $performacehud, icon: "speedometer", label: "Performance Overlay")
+ }
+ }
+
+ // Aspect ratio card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ labelWithIcon("Aspect Ratio", iconName: "rectangle.expand.vertical")
+ .font(.headline)
+
+ if (horizontalSizeClass == .regular && verticalSizeClass == .regular) || (horizontalSizeClass == .regular && verticalSizeClass == .compact) {
+ Picker(selection: config.aspectRatio) {
+ ForEach(AspectRatio.allCases, id: \.self) { ratio in
+ Text(ratio.displayName).tag(ratio)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.segmented)
+ } else {
+ Picker(selection: config.aspectRatio) {
+ ForEach(AspectRatio.allCases, id: \.self) { ratio in
+ Text(ratio.displayName).tag(ratio)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.menu)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.vertical, 4)
+ }
+ }
+ }
+ }
+ }
+
+ // MARK: - Input Settings
+
+ private var inputSettings: some View {
+ SettingsSection(title: "Input Configuration") {
+ // Controller selection card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ Text("Controller Selection")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ if currentControllers.isEmpty {
+ emptyControllersView
+ } else {
+ controllerListView
+ }
+
+ if hasAvailableControllers {
+ Divider()
+ addControllerButton
+ }
+ }
+ }
+
+ // On-screen controls card
+ SettingsCard {
+ VStack(spacing: 4) {
+ SettingsToggle(isOn: config.handHeldController, icon: "formfitting.gamecontroller", label: "Player 1 to Handheld")
+
+ Divider()
+
+ SettingsToggle(isOn: $stickButton, icon: "l.joystick.press.down", label: "Show Stick Buttons")
+
+ Divider()
+
+ SettingsToggle(isOn: $ryuDemo, icon: "hand.draw", label: "On-Screen Controller (Demo)")
+ .disabled(true)
+
+ Divider()
+
+ 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)
+ }
+ }
+
+ // Controller scale card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ Text("On-Screen Controller")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ Group {
+ HStack {
+ labelWithIcon("Scale", iconName: "magnifyingglass")
+ .font(.headline)
+ Spacer()
+ Button {
+ showControllerInfo.toggle()
+ } label: {
+ Image(systemName: "info.circle")
+ .foregroundColor(.secondary)
+ }
+ .buttonStyle(.plain)
+ .alert(isPresented: $showControllerInfo) {
+ Alert(
+ title: Text("On-Screen Controller Scale"),
+ message: Text("Adjust the On-Screen Controller size."),
+ dismissButton: .default(Text("OK"))
+ )
+ }
+ }
+
+ VStack(spacing: 8) {
+ Slider(value: $controllerScale, in: 0.1...3.0, step: 0.05)
+
+ HStack {
+ Text("Smaller")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+
+ Spacer()
+
+ Text("\(controllerScale, specifier: "%.2f")x")
+ .font(.headline)
+ .foregroundColor(.blue)
+
+ Spacer()
+
+ Text("Larger")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+
+ Divider()
+
+ Group {
+ HStack {
+ labelWithIcon("Opacity", iconName: "magnifyingglass")
+ .font(.headline)
+ Spacer()
+ Button {
+ showControllerInfo.toggle()
+ } label: {
+ Image(systemName: "info.circle")
+ .foregroundColor(.secondary)
+ }
+ .buttonStyle(.plain)
+ .alert(isPresented: $showControllerInfo) {
+ Alert(
+ title: Text("On-Screen Controller Opacity"),
+ message: Text("Adjust the On-Screen Controller transparency."),
+ dismissButton: .default(Text("OK"))
+ )
+ }
+ }
+
+ VStack(spacing: 8) {
+ Slider(value: $controllerOpacity, in: 0.1...1.0, step: 0.05)
+
+ HStack {
+ Text("More Transparent")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+
+ Spacer()
+
+ Text("\(controllerOpacity, specifier: "%.2f")x")
+ .font(.headline)
+ .foregroundColor(.blue)
+
+ Spacer()
+
+ Text("Less Transparent")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // MARK: - Controller Selection Components
+
+ private var hasAvailableControllers: Bool {
+ !controllersList.filter { !currentControllers.contains($0) }.isEmpty
+ }
+
+ private var emptyControllersView: some View {
+ HStack {
+ Text("No controllers selected (Keyboard will be used)")
+ .foregroundColor(.secondary)
+ .italic()
+ Spacer()
+ }
+ .padding(.vertical, 8)
+ }
+
+ private var controllerListView: some View {
+ VStack(spacing: 0) {
+ Divider()
+
+ ForEach(currentControllers.indices, id: \.self) { index in
+ let controller = currentControllers[index]
+
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "gamecontroller.fill")
+ .foregroundColor(.blue)
+
+ Text("Player \(index + 1): \(controller.name)")
+ .lineLimit(1)
+
+ Spacer()
+
+ Button {
+ toggleController(controller)
+ } label: {
+ Image(systemName: "xmark.circle.fill")
+ .foregroundColor(.secondary)
+ }
+ .buttonStyle(.plain)
+ }
+ .padding(.vertical, 8)
+
+ if index < currentControllers.count - 1 {
+ Divider()
+ }
+ }
+ }
+ .onMove { from, to in
+ currentControllers.move(fromOffsets: from, toOffset: to)
+ }
+ .environment(\.editMode, .constant(.active))
+ }
+ }
+
+ private var addControllerButton: some View {
+ Menu {
+ ForEach(controllersList.filter { !currentControllers.contains($0) }) { controller in
+ Button {
+ currentControllers.append(controller)
+ } label: {
+ Text(controller.name)
+ }
+ }
+ } label: {
+ Label("Add Controller", systemImage: "plus.circle.fill")
+ .foregroundColor(.blue)
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding(.vertical, 6)
+ }
+ }
+
+ // MARK: - System Settings
+
+ private var systemSettings: some View {
+ SettingsSection(title: "System Configuration") {
+ // Language and region card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 16) {
+ VStack(alignment: .leading, spacing: 8) {
+ labelWithIcon("System Language", iconName: "character.bubble")
+ .font(.headline)
+
+ Picker(selection: config.language) {
+ ForEach(SystemLanguage.allCases, id: \.self) { language in
+ Text(language.displayName).tag(language)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.menu)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.vertical, 4)
+ }
+
+ Divider()
+
+ VStack(alignment: .leading, spacing: 8) {
+ labelWithIcon("Region", iconName: "globe")
+ .font(.headline)
+
+ Picker(selection: config.regioncode) {
+ ForEach(SystemRegionCode.allCases, id: \.self) { region in
+ Text(region.displayName).tag(region)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.menu)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.vertical, 4)
+ }
+ }
+ }
+
+ // CPU options card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 16) {
+ Text("CPU Configuration")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Memory Manager Mode")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+
+ Picker(selection: config.memoryManagerMode) {
+ ForEach(filteredMemoryModes, id: \.0) { key, displayName in
+ Text(displayName).tag(key)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.segmented)
+ }
+
+ Divider()
+
+ SettingsToggle(isOn: config.disablePTC, icon: "cpu", label: "Disable PTC")
+
+ if let gpuInfo = getGPUInfo(), gpuInfo.hasPrefix("Apple M") {
+ Divider()
+
+ if #available(iOS 16.4, *) {
+ SettingsToggle(isOn: .constant(false), icon: "bolt", label: "Hypervisor")
+ .disabled(true)
+ } else if checkAppEntitlement("com.apple.private.hypervisor") {
+ SettingsToggle(isOn: config.hypervisor, icon: "bolt", label: "Hypervisor")
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // MARK: - Advanced Settings
+
+ private var advancedSettings: some View {
+ SettingsSection(title: "Advanced Options") {
+ // Debug options card
+ SettingsCard {
+ VStack(spacing: 4) {
+ SettingsToggle(isOn: $showlogsloading, icon: "text.alignleft", label: "Show Logs While Loading")
+
+ Divider()
+
+ SettingsToggle(isOn: $showlogsgame, icon: "text.magnifyingglass", label: "Show Logs In-Game")
+
+ Divider()
+
+ SettingsToggle(isOn: config.debuglogs, icon: "exclamationmark.bubble", label: "Debug Logs")
+
+ Divider()
+
+ SettingsToggle(isOn: config.tracelogs, icon: "waveform.path", label: "Trace Logs")
+ }
+ }
+
+ // Advanced toggles card
+ SettingsCard {
+ VStack(spacing: 4) {
+ SettingsToggle(isOn: $runOnMainThread, icon: "square.stack.3d.up", label: "Run Core on Main Thread")
+
+ Divider()
+
+ SettingsToggle(isOn: config.dfsIntegrityChecks, icon: "checkmark.shield", label: "Disable FS Integrity Checks")
+
+ Divider()
+
+
+ if MTLHud.shared.canMetalHud {
+ SettingsToggle(isOn: $metalHudEnabler.metalHudEnabled, icon: "speedometer", label: "Metal Performance HUD")
+
+ Divider()
+ }
+
+ SettingsToggle(isOn: $ignoreJIT, icon: "cpu", label: "Ignore JIT Popup")
+
+ Divider()
+
+ Button {
+ finishedStorage = false
+ } label: {
+ HStack {
+ Image(systemName: "arrow.triangle.2.circlepath.circle.fill")
+ .foregroundColor(.blue)
+ Text("Show Setup Screen")
+ .foregroundColor(.blue)
+ Spacer()
+ }
+ .padding(8)
+ }
+
+ }
+ }
+
+ // Memory hacks card
+ SettingsCard {
+ VStack(spacing: 4) {
+ SettingsToggle(isOn: config.expandRam, icon: "exclamationmark.bubble", label: "Expand Guest RAM")
+ .accentColor(.red)
+ .disabled(totalMemory < 5723)
+
+ Divider()
+
+ SettingsToggle(isOn: config.ignoreMissingServices, icon: "waveform.path", label: "Ignore Missing Services")
+ .accentColor(.red)
+ }
+ }
+
+ // Additional args card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ Text("Additional Arguments")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ let binding = Binding(
+ get: {
+ config.additionalArgs.wrappedValue.joined(separator: ", ")
+ },
+ set: { newValue in
+ settingsManager.config.additionalArgs = newValue
+ .split(separator: ",")
+ .map { $0.trimmingCharacters(in: .whitespaces) }
+ }
+ )
+
+
+ if #available(iOS 15.0, *) {
+ TextField("Separate arguments with commas", text: binding)
+ .font(.system(.body, design: .monospaced))
+ .textFieldStyle(.roundedBorder)
+ .textInputAutocapitalization(.none)
+ .disableAutocorrection(true)
+ .padding(.vertical, 4)
+ .toolbar {
+ ToolbarItem(placement: .keyboard) {
+ Button("Dismiss") {
+ isArgumentsKeyboardVisible = false
+ }
+ }
+ }
+ .focused($isArgumentsKeyboardVisible)
+ } else {
+ TextField("Separate arguments with commas", text: binding)
+ .font(.system(.body, design: .monospaced))
+ .textFieldStyle(.roundedBorder)
+ .disableAutocorrection(true)
+ .padding(.vertical, 4)
+ }
+ }
+ }
+
+ // Page size info card
+ SettingsCard {
+ HStack {
+ labelWithIcon("Page Size", iconName: "textformat.size")
+ Spacer()
+ Text("\(String(Int(getpagesize())))")
+ .font(.system(.body, design: .monospaced))
+ .foregroundColor(.secondary)
+ }
+ }
+
+ if gamepo {
+ SettingsCard {
+ Text("The cake is a lie")
+ .font(.system(.body, design: .monospaced))
+ .foregroundColor(.secondary)
+ .frame(maxWidth: .infinity, alignment: .center)
+ }
+ }
+ }
+ }
+
+ // MARK: - Miscellaneous Settings
+
+ private var miscSettings: some View {
+ SettingsSection(title: "Miscellaneous Options") {
+ SettingsCard {
+ VStack(spacing: 4) {
+ if UIDevice.current.userInterfaceIdiom == .pad {
+ SettingsToggle(isOn: $toggleGreen, icon: "arrow.clockwise", label: "Toggle Color Green when \"ON\"")
+
+ Divider()
+ }
+
+
+ // Disable Touch card
+ SettingsToggle(isOn: $disableTouch, icon: "rectangle.and.hand.point.up.left.filled", label: "Disable Touch")
+
+ Divider()
+
+ if colorScheme == .light {
+ SettingsToggle(isOn: $blackScreen, icon: "iphone.slash", label: "Black Screen when using AirPlay")
+
+ Divider()
+ }
+
+ Button {
+ showAppIconSwitcher = true
+ } label: {
+ HStack {
+ Image(systemName: "app.dashed")
+ .foregroundColor(.blue)
+ Text("App Icon Switcher")
+ .foregroundColor(.primary)
+ Spacer()
+ }
+ .padding(.vertical, 8)
+ }
+ .sheet(isPresented: $showAppIconSwitcher) {
+ AppIconSwitcherView()
+ }
+
+ Divider()
+
+ // Exit button card
+ SettingsToggle(isOn: $ssb, icon: "arrow.left.circle", label: "Menu Button (in-game)")
+
+ Divider()
+
+ // Restarts app when it crashes card
+ SettingsToggle(isOn: $restartApp, icon: "arrow.clockwise", label: "Lock in App")
+
+ Divider()
+
+
+ // Location to keep app in Background
+ SettingsToggle(isOn: $locationenabled, icon: "location.viewfinder", label: "Keep app in background")
+
+ Divider()
+
+ if UIDevice.current.userInterfaceIdiom == .pad {
+ // Old Settings UI
+ SettingsToggle(isOn: $oldSettingsUI, icon: "ipad.landscape", label: "Non Switch-like Settings")
+
+ Divider()
+ }
+
+
+ // JIT options
+ if #available(iOS 17.0.1, *) {
+ let checked = stikJITorStikDebug()
+ let stikJIT = checked == 1 ? "StikDebug" : checked == 2 ? "StikJIT" : "StikDebug"
+
+ SettingsToggle(isOn: $stikJIT, icon: "bolt.heart", label: stikJIT)
+ .contextMenu {
+ Button {
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+ let mainWindow = windowScene.windows.last {
+ let alertController = UIAlertController(title: "About \(stikJIT)", message: "\(stikJIT) is a really amazing iOS Application to Enable JIT on the go on-device, made by the best, most kind, helpful and nice developers of all time jkcoxson and Blu <3", preferredStyle: .alert)
+
+ let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in
+ UIApplication.shared.open(URL(string: "https://github.com/StephenDev0/StikJIT")!)
+ }
+ alertController.addAction(learnMoreButton)
+
+ let doneButton = UIAlertAction(title: "Done", style: .cancel, handler: nil)
+ alertController.addAction(doneButton)
+
+ mainWindow.rootViewController?.present(alertController, animated: true)
+ }
+ } label: {
+ Text("About")
+ }
+ }
+ } else {
+ SettingsToggle(isOn: $useTrollStore, icon: "troll.svg", label: "TrollStore JIT")
+ }
+
+ Divider()
+
+ // MoltenVK Options
+ SettingsToggle(isOn: $syncqsubmits, icon: "line.diagonal", label: "MVK: Synchronous Queue Submits")
+ .contextMenu {
+ Button {
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+ let mainWindow = windowScene.windows.last {
+ let alertController = UIAlertController(title: "About MVK: Synchronous Queue Submits", message: "Enable this option if Mario Kart 8 is crashing at Grand Prix mode.", preferredStyle: .alert)
+
+ let doneButton = UIAlertAction(title: "OK", style: .cancel, handler: nil)
+ alertController.addAction(doneButton)
+
+ mainWindow.rootViewController?.present(alertController, animated: true)
+ }
+ } label: {
+ Text("About")
+ }
+ }
+
+ Divider()
+
+ SettingsToggle(isOn: $checkForUpdate, icon: "square.and.arrow.down", label: "Check for Updates")
+
+ if ryujinx.firmwareversion != "0" {
+ Divider()
+ Button {
+ Ryujinx.shared.removeFirmware()
+ } label: {
+ HStack {
+ Text("Remove Firmware")
+ .foregroundColor(.blue)
+ Spacer()
+ }
+ .padding(.vertical, 8)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // MARK: - Helper Functions
+
+ private func toggleController(_ controller: Controller) {
+ if currentControllers.contains(where: { $0.id == controller.id }) {
+ currentControllers.removeAll(where: { $0.id == controller.id })
+ } else {
+ currentControllers.append(controller)
+ }
+ }
+
+
+ func getGPUInfo() -> String? {
+ let device = MTLCreateSystemDefaultDevice()
+ return device?.name
+ }
+
+ @ViewBuilder
+ private func labelWithIcon(_ text: String, iconName: String, flipimage: Bool? = nil) -> some View {
+ HStack(spacing: 8) {
+ if iconName.hasSuffix(".svg") {
+ if let flipimage, flipimage {
+ SVGView(svgName: iconName, color: .blue)
+ // .symbolRenderingMode(.hierarchical)
+ .frame(width: 20, height: 20)
+ .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
+ } else {
+ SVGView(svgName: iconName, color: .blue)
+ // .symbolRenderingMode(.hierarchical)
+ .frame(width: 20, height: 20)
+ }
+ } else if !iconName.isEmpty {
+ Image(systemName: iconName)
+ // .symbolRenderingMode(.hierarchical)
+ .foregroundColor(.blue)
+ }
+ Text(text)
+ }
+ .font(.body)
+ }
+}
+
+struct SVGView: UIViewRepresentable {
+ var svgName: String
+ var color: Color = Color.black
+
+ func makeUIView(context: Context) -> UIView {
+ var svgName = svgName
+ let hammock = UIView()
+
+ if svgName.hasSuffix(".svg") {
+ svgName.removeLast(4)
+ }
+
+ _ = UIView(svgNamed: svgName) { svgLayer in
+ svgLayer.fillColor = UIColor(color).cgColor // Apply the provided color
+ svgLayer.resizeToFit(hammock.frame)
+ hammock.layer.addSublayer(svgLayer)
+ }
+
+ return hammock
+ }
+
+ func updateUIView(_ uiView: UIView, context: Context) {
+ // Update the SVG view's fill color when the color changes
+ if let svgLayer = uiView.layer.sublayers?.first as? CAShapeLayer {
+ svgLayer.fillColor = UIColor(color).cgColor
+ }
+ }
+}
+
+func saveSettings(config: Ryujinx.Arguments) {
+ do {
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = .prettyPrinted
+ let data = try encoder.encode(config)
+
+ let fileURL = URL.documentsDirectory.appendingPathComponent("config.json")
+
+ try data.write(to: fileURL)
+ // print("Settings saved to: \(fileURL.path)")
+ } catch {
+ // print("Failed to save settings: \(error)")
+ }
+}
+
+func loadSettings() -> Ryujinx.Arguments? {
+ do {
+ let fileURL = URL.documentsDirectory.appendingPathComponent("config.json")
+
+ guard FileManager.default.fileExists(atPath: fileURL.path) else {
+ // print("Config file does not exist at: \(fileURL.path)")
+ return nil
+ }
+
+ let data = try Data(contentsOf: fileURL)
+
+ let decoder = JSONDecoder()
+ let configs = try decoder.decode(Ryujinx.Arguments.self, from: data)
+ return configs
+ } catch {
+ // print("Failed to load settings: \(error)")
+ return nil
+ }
+}
+
+
+// MARK: - Supporting Views
+
+struct CategoryButton: View {
+ let title: String
+ let icon: String
+ let isSelected: Bool
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ VStack(spacing: 6) {
+ Image(systemName: icon)
+ .font(.system(size: 16, weight: isSelected ? .semibold : .regular))
+ Text(title)
+ .font(.system(size: 12, weight: isSelected ? .semibold : .regular))
+ }
+ .foregroundColor(isSelected ? .blue : .secondary)
+ .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)
+ }
+ }
+}
+
+struct SettingsSection: View {
+ let title: String
+ let content: Content
+
+ init(title: String, @ViewBuilder content: () -> Content) {
+ self.title = title
+ self.content = content()
+ }
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ Text(title)
+ .font(.title2.weight(.bold))
+ .padding(.horizontal)
+
+ content
+ }
+ }
+}
+
+struct SettingsCard: View {
+ @Environment(\.colorScheme) var colorScheme
+ @AppStorage("oldSettingsUI") var oldSettingsUI = false
+ let content: Content
+
+ init(@ViewBuilder content: () -> Content) {
+ self.content = content()
+ }
+
+ var body: some View {
+ if UIDevice.current.userInterfaceIdiom == .phone || oldSettingsUI {
+ content
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(colorScheme == .dark ? Color(.systemGray6) : Color.white)
+ .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
+ )
+ .padding(.horizontal)
+ } else {
+ VStack {
+ Divider()
+ content
+ Divider()
+ }
+ .padding()
+ }
+ }
+}
+
+struct SettingsToggle: View {
+ @Binding var isOn: Bool
+ let icon: String
+ let label: String
+ var disabled: Bool = false
+ @AppStorage("toggleGreen") var toggleGreen: Bool = false
+ @AppStorage("oldSettingsUI") var oldSettingsUI = false
+
+ var body: some View {
+ if UIDevice.current.userInterfaceIdiom == .phone || oldSettingsUI {
+ Toggle(isOn: $isOn) {
+ HStack(spacing: 8) {
+ if icon.hasSuffix(".svg") {
+ SVGView(svgName: icon, color: .blue)
+ .frame(width: 20, height: 20)
+ } else {
+ Image(systemName: icon)
+ // .symbolRenderingMode(.hierarchical)
+ .foregroundColor(.blue)
+ }
+
+ Text(label)
+ .font(.body)
+ }
+ }
+ .toggleStyle(SwitchToggleStyle(tint: .blue))
+ .disabled(disabled)
+ .padding(.vertical, 6)
+ } else {
+ Group {
+ HStack(spacing: 8) {
+ HStack {
+ if icon.hasSuffix(".svg") {
+ SVGView(svgName: icon, color: .blue)
+ .frame(width: 20, height: 20)
+ } else {
+ Image(systemName: icon)
+ // .symbolRenderingMode(.hierarchical)
+ .foregroundStyle(.blue)
+ }
+
+ Text(label)
+ .font(.body)
+ }
+
+ Spacer()
+
+
+ Text(isOn ? "ON" : "Off")
+ .foregroundStyle(isOn ? (toggleGreen ? .green : .blue) : .blue)
+ }
+ .padding()
+ .onTapGesture {
+ isOn.toggle()
+ }
+ }
+ }
+ }
+
+ func disabled(_ disabled: Bool) -> SettingsToggle {
+ var view = self
+ view.disabled = disabled
+ return view
+ }
+
+ func accentColor(_ color: Color) -> SettingsToggle {
+ var view = self
+ return view
+ }
+}
+
+struct InfoCard: View {
+ let title: String
+ let value: String
+ let icon: String
+ let color: Color
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 4) {
+ HStack {
+ Image(systemName: icon)
+ .foregroundColor(color)
+ Text(title)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ Text(value)
+ .font(.system(size: 14, weight: .medium))
+ .lineLimit(1)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(10)
+ .background(color.opacity(0.1))
+ .cornerRadius(8)
+ }
+}
+
+// this code is used to enable the keyboard to be dismissed when scrolling if available on iOS 16+
+extension View {
+ @ViewBuilder
+ func scrollDismissesKeyboardIfAvailable() -> some View {
+ if #available(iOS 16.0, *) {
+ self.scrollDismissesKeyboard(.interactively)
+ } else {
+ self
+ }
+ }
+}
+
diff --git a/src/MeloNX/MeloNX/App/Views/TabView/TabView.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/TabView/TabView.swift
similarity index 66%
rename from src/MeloNX/MeloNX/App/Views/TabView/TabView.swift
rename to src/MeloNX/MeloNX/App/Views/Main/UI/TabView/TabView.swift
index ccf7796bc..ac6d33ee7 100644
--- a/src/MeloNX/MeloNX/App/Views/TabView/TabView.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/TabView/TabView.swift
@@ -11,7 +11,6 @@ import UniformTypeIdentifiers
struct MainTabView: View {
@Binding var startemu: Game?
- @Binding var config: Ryujinx.Configuration
@Binding var MVKconfig: [MoltenVKSettings]
@Binding var controllersList: [Controller]
@Binding var currentControllers: [Controller]
@@ -25,7 +24,8 @@ struct MainTabView: View {
Label("Games", systemImage: "gamecontroller.fill")
}
- SettingsView(config: $config, MoltenVKSettings: $MVKconfig, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
+ // SettingsView(config: $config, MoltenVKSettings: $MVKconfig, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
+ SettingsViewNew(MoltenVKSettings: $MVKconfig, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
.tabItem {
Label("Settings", systemImage: "gear")
}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/Updates/App/MeloNXUpdateSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/Updates/App/MeloNXUpdateSheet.swift
new file mode 100644
index 000000000..d2297555e
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/Updates/App/MeloNXUpdateSheet.swift
@@ -0,0 +1,74 @@
+//
+// MeloNXUpdateSheet.swift
+// MeloNX
+//
+// Created by Stossy11 and Bella on 12/03/2025.
+//
+
+import SwiftUI
+
+struct MeloNXUpdateSheet: View {
+ let updateInfo: LatestVersionResponse
+ @Binding var isPresented: Bool
+
+ var body: some View {
+ iOSNav {
+ VStack {
+ Text("Version \(updateInfo.version_number) is available. You are currently on Version \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown").")
+
+ VStack {
+ Text("Changelog:")
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .font(.headline)
+
+ ScrollView {
+ Text(updateInfo.changelog)
+ .padding()
+ }
+ .frame(maxHeight: 400)
+ .background(Color(.secondarySystemBackground))
+ .clipShape(RoundedRectangle(cornerRadius: 10))
+ }
+ .padding(.top, 15)
+
+
+ Spacer()
+ if #available(iOS 15.0, *) {
+ Button(action: {
+ if let url = URL(string: updateInfo.download_link) {
+ UIApplication.shared.open(url)
+ }
+ }) {
+ Text("Download Now")
+ .font(.title3)
+ .bold()
+ .frame(width: 300, height: 40)
+ }
+ .buttonStyle(.borderedProminent)
+ .frame(alignment: .bottom)
+ } else {
+ Button(action: {
+ if let url = URL(string: updateInfo.download_link) {
+ UIApplication.shared.open(url)
+ }
+ }) {
+ Text("Download Now")
+ .font(.title3)
+ .bold()
+ .frame(width: 300, height: 40)
+ }
+ .frame(alignment: .bottom)
+ }
+ }
+ .padding(.horizontal)
+ .navigationTitle("Version \(updateInfo.version_number) Available!")
+ .toolbar {
+ Button(action: {
+ isPresented = false
+ }) {
+ Text("Close")
+ }
+ }
+ }
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/Updates/Games/GameDLCManagerSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/Updates/Games/GameDLCManagerSheet.swift
new file mode 100644
index 000000000..b479e4c2f
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/Updates/Games/GameDLCManagerSheet.swift
@@ -0,0 +1,337 @@
+//
+// GameDLCManagerSheet.swift
+// MeloNX
+//
+// Created by XITRIX on 16/02/2025.
+//
+
+import SwiftUI
+import UniformTypeIdentifiers
+
+// MARK: - Models
+struct DownloadableContentNca: Codable, Hashable {
+ var fullPath: String
+ var titleId: UInt
+ var enabled: Bool
+
+ enum CodingKeys: String, CodingKey {
+ case fullPath = "path"
+ case titleId = "title_id"
+ case enabled = "is_enabled"
+ }
+}
+
+struct DownloadableContentContainer: Codable, Hashable, Identifiable {
+ var id: String { containerPath }
+ var containerPath: String
+ var downloadableContentNcaList: [DownloadableContentNca]
+
+ var filename: String {
+ (containerPath as NSString).lastPathComponent
+ }
+
+ var isEnabled: Bool {
+ downloadableContentNcaList.first?.enabled == true
+ }
+
+ enum CodingKeys: String, CodingKey {
+ case containerPath = "path"
+ case downloadableContentNcaList = "dlc_nca_list"
+ }
+}
+
+// MARK: - View
+struct DLCManagerSheet: View {
+ // MARK: - Properties
+ @Binding var game: Game!
+ @State private var isSelectingGameDLC = false
+ @State private var dlcs: [DownloadableContentContainer] = []
+ @Environment(\.presentationMode) var presentationMode
+
+ // MARK: - Body
+ var body: some View {
+ iOSNav {
+ List {
+ if dlcs.isEmpty {
+ emptyStateView
+ } else {
+ ForEach(dlcs) { dlc in
+ dlcRow(dlc)
+ }
+ .onDelete(perform: removeDLCs)
+ }
+ }
+ .navigationTitle("\(game.titleName) DLCs")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarLeading) {
+ Button("Done") {
+ presentationMode.wrappedValue.dismiss()
+ }
+ }
+
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button {
+ isSelectingGameDLC = true
+ } label: {
+ Label("Add DLC", systemImage: "plus")
+ }
+ }
+ }
+ .onAppear {
+ loadData()
+ }
+ }
+ .fileImporter(
+ isPresented: $isSelectingGameDLC,
+ allowedContentTypes: [.item],
+ allowsMultipleSelection: true,
+ onCompletion: handleFileImport
+ )
+ }
+
+ // MARK: - Views
+ private var emptyStateView: some View {
+ Group {
+ if #available(iOS 17, *) {
+ ContentUnavailableView(
+ "No DLCs Found",
+ systemImage: "puzzlepiece.extension",
+ description: Text("Tap the + button to add game DLCs.")
+ )
+ } else {
+ VStack(spacing: 20) {
+ Spacer()
+
+ Image(systemName: "puzzlepiece.extension")
+ .font(.system(size: 64))
+ .foregroundColor(.secondary)
+
+ Text("No DLCs Found")
+ .font(.title2)
+ .fontWeight(.semibold)
+
+ Text("Tap the + button to add game DLCs.")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.center)
+ .padding(.horizontal)
+
+ Spacer()
+ }
+ .frame(maxWidth: .infinity)
+ .listRowInsets(EdgeInsets())
+ }
+ }
+ }
+
+
+ private func dlcRow(_ dlc: DownloadableContentContainer) -> some View {
+ Group {
+ if #available(iOS 15.0, *) {
+ Button {
+ toggleDLC(dlc)
+ } label: {
+ HStack {
+ Text(dlc.filename)
+ .foregroundColor(.primary)
+ Spacer()
+ Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle")
+ .foregroundColor(dlc.isEnabled ? .primary : .secondary)
+ .imageScale(.large)
+ }
+ .contentShape(Rectangle())
+ }
+ .buttonStyle(.plain)
+ .swipeActions(edge: .trailing) {
+ Button(role: .destructive) {
+ if let index = dlcs.firstIndex(where: { $0.id == dlc.id }) {
+ removeDLC(at: IndexSet(integer: index))
+ }
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
+ }
+ } else {
+ Button {
+ toggleDLC(dlc)
+ } label: {
+ HStack {
+ Text(dlc.filename)
+ .foregroundColor(.primary)
+ Spacer()
+ Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle")
+ .foregroundColor(dlc.isEnabled ? .primary : .secondary)
+ .imageScale(.large)
+ }
+ .contentShape(Rectangle())
+ }
+ .buttonStyle(.plain)
+ .contextMenu {
+ Button {
+ if let index = dlcs.firstIndex(where: { $0.id == dlc.id }) {
+ removeDLC(at: IndexSet(integer: index))
+ }
+ } label: {
+ Label("Delete", systemImage: "trash")
+ .foregroundColor(.red)
+ }
+ }
+ }
+ }
+ }
+
+ // MARK: - Functions
+ private func loadData() {
+ dlcs = Self.loadDlc(game)
+ }
+
+ private func toggleDLC(_ dlc: DownloadableContentContainer) {
+ guard let index = dlcs.firstIndex(where: { $0.id == dlc.id }) else { return }
+
+ let toggle = !dlcs[index].isEnabled
+ dlcs[index].downloadableContentNcaList = dlcs[index].downloadableContentNcaList.map { nca in
+ var mutableNca = nca
+ mutableNca.enabled = toggle
+ return mutableNca
+ }
+
+ Self.saveDlcs(game, dlc: dlcs)
+ }
+
+ private func removeDLCs(at offsets: IndexSet) {
+ offsets.forEach { removeDLC(at: IndexSet(integer: $0)) }
+ }
+
+ private func removeDLC(at indexSet: IndexSet) {
+ guard let index = indexSet.first else { return }
+
+ let dlcToRemove = dlcs[index]
+ let path = URL.documentsDirectory.appendingPathComponent(dlcToRemove.containerPath)
+
+ do {
+ try FileManager.default.removeItem(at: path)
+ dlcs.remove(at: index)
+ Self.saveDlcs(game, dlc: dlcs)
+ } catch {
+ print("Failed to remove DLC: \(error)")
+ }
+ }
+
+ private func handleFileImport(result: Result<[URL], Error>) {
+ switch result {
+ case .success(let urls):
+ for url in urls {
+ importDLC(from: url)
+ }
+ case .failure(let error):
+ print("File import failed: \(error.localizedDescription)")
+ }
+ }
+
+ private func importDLC(from url: URL) {
+ guard url.startAccessingSecurityScopedResource() else {
+ print("Failed to access security-scoped resource")
+ return
+ }
+ defer { url.stopAccessingSecurityScopedResource() }
+
+ do {
+ let fileManager = FileManager.default
+ let dlcDirectory = URL.documentsDirectory.appendingPathComponent("dlc")
+ let gameDlcDirectory = dlcDirectory.appendingPathComponent(game.titleId)
+
+ try fileManager.createDirectory(at: gameDlcDirectory, withIntermediateDirectories: true)
+
+ // Copy the DLC file
+ let destinationURL = gameDlcDirectory.appendingPathComponent(url.lastPathComponent)
+ try? fileManager.removeItem(at: destinationURL)
+ try fileManager.copyItem(at: url, to: destinationURL)
+
+ // Fetch DLC metadata from Ryujinx
+ let dlcContent = Ryujinx.shared.getDlcNcaList(titleId: game.titleId, path: destinationURL.path)
+ guard !dlcContent.isEmpty else {
+ print("No valid DLC content found")
+ return
+ }
+
+ let newDlcContainer = DownloadableContentContainer(
+ containerPath: Self.relativeDlcDirectoryPath(for: game, dlcPath: destinationURL),
+ downloadableContentNcaList: dlcContent
+ )
+
+ dlcs.append(newDlcContainer)
+ Self.saveDlcs(game, dlc: dlcs)
+
+ } catch {
+ print("Error importing DLC: \(error)")
+ }
+ }
+
+}
+
+// MARK: - Helper Methods
+private extension DLCManagerSheet {
+ static func loadDlc(_ game: Game) -> [DownloadableContentContainer] {
+ let jsonURL = dlcJsonPath(for: game)
+
+ do {
+ try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
+
+ guard FileManager.default.fileExists(atPath: jsonURL.path),
+ let data = try? Data(contentsOf: jsonURL),
+ var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data)
+ else { return [] }
+
+ result = result.filter { container in
+ let path = URL.documentsDirectory.appendingPathComponent(container.containerPath)
+ return FileManager.default.fileExists(atPath: path.path)
+ }
+
+ return result
+ } catch {
+ // print("Error loading DLCs: \(error)")
+ return []
+ }
+ }
+
+ static func saveDlcs(_ game: Game, dlc: [DownloadableContentContainer]) {
+ do {
+ let data = try JSONEncoder().encode(dlc)
+ try data.write(to: dlcJsonPath(for: game))
+ } catch {
+ print("Error saving DLCs: \(error)")
+ }
+ }
+
+ static func relativeDlcDirectoryPath(for game: Game, dlcPath: URL) -> String {
+ "dlc/\(game.titleId)/\(dlcPath.lastPathComponent)"
+ }
+
+ static func dlcJsonPath(for game: Game) -> URL {
+ URL.documentsDirectory
+ .appendingPathComponent("games")
+ .appendingPathComponent(game.titleId)
+ .appendingPathComponent("dlc.json")
+ }
+}
+
+// MARK: - Array Extension
+extension Array where Element: AnyObject {
+ mutating func mutableForEach(_ body: (inout Element) -> Void) {
+ for index in indices {
+ var element = self[index]
+ body(&element)
+ self[index] = element
+ }
+ }
+}
+
+// MARK: - URL Extension
+extension URL {
+ @available(iOS, introduced: 14.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above")
+ static var documentsDirectory: URL {
+ let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
+ return documentDirectory
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/Updates/Games/GameUpdateManagerSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/Updates/Games/GameUpdateManagerSheet.swift
new file mode 100644
index 000000000..6ee97b6f7
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/Updates/Games/GameUpdateManagerSheet.swift
@@ -0,0 +1,334 @@
+//
+// GameUpdateManagerSheet.swift
+// MeloNX
+//
+// Created by Stossy11 on 16/02/2025.
+//
+
+import SwiftUI
+import UniformTypeIdentifiers
+
+struct UpdateManagerSheet: View {
+ // MARK: - Properties
+ @State private var updates: [UpdateItem] = []
+ @Binding var game: Game?
+ @State private var isSelectingGameUpdate = false
+ @State private var jsonURL: URL? = nil
+ @Environment(\.presentationMode) var presentationMode
+
+ // MARK: - Models
+ class UpdateItem: Identifiable, ObservableObject {
+ let id = UUID()
+ let url: URL
+ let filename: String
+ let path: String
+
+ @Published var isSelected: Bool = false
+
+ init(url: URL, filename: String, path: String, isSelected: Bool = false) {
+ self.url = url
+ self.filename = filename
+ self.path = path
+ self.isSelected = isSelected
+ }
+ }
+
+ // MARK: - Body
+ var body: some View {
+ iOSNav {
+ List {
+ if updates.isEmpty {
+ emptyStateView
+ } else {
+ ForEach(updates) { update in
+ updateRow(update)
+ }
+ .onDelete(perform: removeUpdates)
+ }
+ }
+ .navigationTitle("\(game?.titleName ?? "Game") Updates")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarLeading) {
+ Button("Done") {
+ presentationMode.wrappedValue.dismiss()
+ }
+ }
+
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button {
+ isSelectingGameUpdate = true
+ } label: {
+ Label("Add Update", systemImage: "plus")
+ }
+ }
+ }
+ .onAppear {
+ loadData()
+ }
+ }
+ .fileImporter(isPresented: $isSelectingGameUpdate, allowedContentTypes: [.item], onCompletion: handleFileImport)
+ }
+
+ // MARK: - Views
+ private var emptyStateView: some View {
+ Group {
+ if #available(iOS 17, *) {
+ ContentUnavailableView(
+ "No Updates Found",
+ systemImage: "arrow.down.circle",
+ description: Text("Tap the + button to add game updates.")
+ )
+ } else {
+ VStack(spacing: 20) {
+ Spacer()
+
+ Image(systemName: "arrow.down.circle")
+ .font(.system(size: 64))
+ .foregroundColor(.secondary)
+
+ Text("No Updates Found")
+ .font(.title2)
+ .fontWeight(.semibold)
+
+ Text("Tap the + button to add game updates.")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.center)
+ .padding(.horizontal)
+
+ Spacer()
+ }
+ .frame(maxWidth: .infinity)
+ .listRowInsets(EdgeInsets())
+ }
+ }
+ }
+
+ private func updateRow(_ update: UpdateItem) -> some View {
+ Group {
+ if #available(iOS 15, *) {
+ updateRowNew(update)
+ } else {
+ updateRowOld(update)
+ }
+ }
+ }
+
+ @available(iOS 15, *)
+ private func updateRowNew(_ update: UpdateItem) -> some View {
+ Button {
+ toggleSelection(update)
+ } label: {
+ HStack {
+ Text(update.filename)
+ .foregroundColor(.primary)
+ Spacer()
+ Image(systemName: update.isSelected ? "checkmark.circle.fill" : "circle")
+ .foregroundColor(update.isSelected ? .primary : .secondary)
+ .imageScale(.large)
+ }
+ .contentShape(Rectangle())
+ }
+ .buttonStyle(.plain)
+ .swipeActions(edge: .trailing) {
+ Button(role: .destructive) {
+ if let index = updates.firstIndex(where: { $0.path == update.path }) {
+ removeUpdate(at: IndexSet(integer: index))
+ }
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
+ }
+ }
+
+ private func updateRowOld(_ update: UpdateItem) -> some View {
+ Button {
+ toggleSelection(update)
+ } label: {
+ HStack {
+ Text(update.filename)
+ .foregroundColor(.primary)
+ Spacer()
+ Image(systemName: update.isSelected ? "checkmark.circle.fill" : "circle")
+ .foregroundColor(update.isSelected ? .primary : .secondary)
+ .imageScale(.large)
+ }
+ .contentShape(Rectangle())
+ }
+ .contextMenu {
+ Button {
+ if let index = updates.firstIndex(where: { $0.path == update.path }) {
+ removeUpdate(at: IndexSet(integer: index))
+ }
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
+ }
+ }
+
+ // MARK: - Functions
+ private func loadData() {
+ guard let game = game else { return }
+
+ let documentsDirectory = URL.documentsDirectory
+ jsonURL = documentsDirectory
+ .appendingPathComponent("games")
+ .appendingPathComponent(game.titleId)
+ .appendingPathComponent("updates.json")
+
+ loadJSON()
+ }
+
+ private func loadJSON() {
+ guard let jsonURL = jsonURL else { return }
+
+ do {
+ if !FileManager.default.fileExists(atPath: jsonURL.path) {
+ createDefaultJSON()
+ return
+ }
+
+ let data = try Data(contentsOf: jsonURL)
+ if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
+ let paths = jsonDict["paths"] as? [String],
+ let selected = jsonDict["selected"] as? String {
+
+ let filteredPaths = paths.filter { relativePath in
+ let path = URL.documentsDirectory.appendingPathComponent(relativePath)
+ return FileManager.default.fileExists(atPath: path.path)
+ }
+
+ updates = filteredPaths.map { relativePath in
+ let url = URL.documentsDirectory.appendingPathComponent(relativePath)
+ return UpdateItem(
+ url: url,
+ filename: url.lastPathComponent,
+ path: relativePath,
+ isSelected: selected == relativePath
+ )
+ }
+ }
+ } catch {
+ print("Failed to read JSON: \(error)")
+ createDefaultJSON()
+ }
+ }
+
+ private func createDefaultJSON() {
+ guard let jsonURL = jsonURL else { return }
+
+ do {
+ try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
+
+ let defaultData: [String: Any] = ["selected": "", "paths": []]
+ let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted)
+ try newData.write(to: jsonURL)
+ updates = []
+ } catch {
+ print("Failed to create default JSON: \(error)")
+ }
+ }
+
+ private func handleFileImport(result: Result) {
+ switch result {
+ case .success(let selectedURL):
+ guard let game = game,
+ selectedURL.startAccessingSecurityScopedResource() else {
+ print("Failed to access security-scoped resource")
+ return
+ }
+
+ defer { selectedURL.stopAccessingSecurityScopedResource() }
+
+ do {
+ let fileManager = FileManager.default
+ let updatesDirectory = URL.documentsDirectory.appendingPathComponent("updates")
+ let gameUpdatesDirectory = updatesDirectory.appendingPathComponent(game.titleId)
+
+ // Create directories if needed
+ try fileManager.createDirectory(at: gameUpdatesDirectory, withIntermediateDirectories: true)
+
+ // Copy the file
+ let destinationURL = gameUpdatesDirectory.appendingPathComponent(selectedURL.lastPathComponent)
+ try? fileManager.removeItem(at: destinationURL) // Remove if exists
+ try fileManager.copyItem(at: selectedURL, to: destinationURL)
+
+ // Add to updates
+ let relativePath = "updates/\(game.titleId)/\(selectedURL.lastPathComponent)"
+ let newUpdate = UpdateItem(
+ url: destinationURL,
+ filename: selectedURL.lastPathComponent,
+ path: relativePath
+ )
+
+ updates.append(newUpdate)
+ toggleSelection(newUpdate)
+
+ // Reload games
+ Ryujinx.shared.games = Ryujinx.shared.loadGames()
+ } catch {
+ print("Error copying update file: \(error)")
+ }
+
+ case .failure(let error):
+ print("File import failed: \(error.localizedDescription)")
+ }
+ }
+
+ private func toggleSelection(_ update: UpdateItem) {
+ print("toggle selection \(update.path)")
+
+ updates = updates.map { item in
+ item.isSelected = item.path == update.path && !update.isSelected
+ // print(mutableItem.isSelected)
+ // print(update.isSelected)
+ return item
+ }
+
+ // print(updates)
+
+ saveJSON()
+ }
+
+ private func removeUpdates(at offsets: IndexSet) {
+ offsets.forEach { removeUpdate(at: IndexSet(integer: $0)) }
+ }
+
+ private func removeUpdate(at indexSet: IndexSet) {
+ guard let index = indexSet.first else { return }
+
+ let updateToRemove = updates[index]
+
+ do {
+ // Remove the file
+ try FileManager.default.removeItem(at: updateToRemove.url)
+
+ // Remove from updates array
+ updates.remove(at: index)
+
+ // Save changes
+ saveJSON()
+
+ // Reload games
+ Ryujinx.shared.games = Ryujinx.shared.loadGames()
+ } catch {
+ print("Failed to remove update: \(error)")
+ }
+ }
+
+ private func saveJSON() {
+ guard let jsonURL = jsonURL else { return }
+
+ do {
+ let paths = updates.map { $0.path }
+ let selected = updates.first(where: { $0.isSelected })?.path ?? ""
+
+ let jsonDict = ["paths": paths, "selected": selected] as [String: Any]
+ let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
+ try newData.write(to: jsonURL)
+ } catch {
+ print("Failed to update JSON: \(error)")
+ }
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift b/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift
new file mode 100644
index 000000000..aeb7e5146
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift
@@ -0,0 +1,150 @@
+//
+// MeloNXApp.swift
+// MeloNX
+//
+// Created by Stossy11 on 3/11/2024.
+//
+
+import SwiftUI
+import UIKit
+import CryptoKit
+import UniformTypeIdentifiers
+import AVFoundation
+
+
+extension UIDocumentPickerViewController {
+ @objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController {
+ return fix_init(forOpeningContentTypes: contentTypes, asCopy: true)
+ }
+}
+
+@main
+struct MeloNXApp: App {
+
+ @State var showed = false
+ @Environment(\.scenePhase) var scenePhase
+ @State var alert: UIAlertController? = nil
+
+ @State var showOutOfDateSheet = false
+ @State var updateInfo: LatestVersionResponse? = nil
+
+ @StateObject var metalHudEnabler = MTLHud.shared
+
+ @State var finished = false
+ @AppStorage("hasbeenfinished") var finishedStorage: Bool = false
+
+ @AppStorage("location-enabled") var locationenabled: Bool = false
+ @AppStorage("checkForUpdate") var checkForUpdate: Bool = true
+
+ @AppStorage("runOnMainThread") var runOnMainThread = false
+
+ @AppStorage("autoJIT") var autoJIT = false
+
+ @State var fourgbiPad = false
+ @AppStorage("4GB iPad") var ignores = false
+ // String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000)
+ var body: some Scene {
+ WindowGroup {
+ Group {
+ if finishedStorage {
+ ContentView()
+ .withFileImporter()
+ .onAppear {
+ if checkForUpdate {
+ checkLatestVersion()
+ }
+
+ print(metalHudEnabler.canMetalHud)
+
+ UserDefaults.standard.set(false, forKey: "lockInApp")
+ }
+ .sheet(isPresented: Binding(
+ get: { showOutOfDateSheet && updateInfo != nil },
+ set: { newValue in
+ if !newValue {
+ showOutOfDateSheet = false
+ updateInfo = nil
+ }
+ }
+ )) {
+ if let updateInfo = updateInfo {
+ MeloNXUpdateSheet(updateInfo: updateInfo, isPresented: $showOutOfDateSheet)
+ }
+ }
+ } else {
+ SetupView(finished: $finished)
+ .onChange(of: finished) { newValue in
+ withAnimation(.easeOut) {
+ finishedStorage = newValue
+ }
+ }
+ }
+ }
+ .onAppear() {
+ if UIDevice.current.userInterfaceIdiom == .pad && !ignores {
+ print((Double(ProcessInfo.processInfo.physicalMemory) / 1_000_000_000))
+ if round(Double(ProcessInfo.processInfo.physicalMemory) / 1_000_000_000) <= 4 {
+ fourgbiPad = true
+ }
+ }
+ }
+ .alert("Unsupported Device", isPresented: $fourgbiPad) {
+ Button("Continue") {
+ ignores = true
+ fourgbiPad = false
+ }
+ } message: {
+ Text("Your Device is an iPad with \(String(format: "%.0f GB", Double(ProcessInfo.processInfo.physicalMemory) / 1_000_000_000)) of memory, MeloNX has issues with those devices")
+ }
+ }
+ }
+
+ func checkLatestVersion() {
+ let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
+ let strippedAppVersion = appVersion.replacingOccurrences(of: ".", with: "")
+
+ #if DEBUG
+ let urlString = "http://192.168.178.116:8000/api/latest_release"
+ #else
+ let urlString = "https://melonx.net/api/latest_release"
+ #endif
+
+ guard let url = URL(string: urlString) else {
+ // print("Invalid URL")
+ return
+ }
+
+ let task = URLSession.shared.dataTask(with: url) { data, response, error in
+ if let error = error {
+ // print("Error checking for new version: \(error)")
+ return
+ }
+
+ guard let data = data else {
+ // print("No data received")
+ return
+ }
+
+ do {
+ let latestVersionResponse = try JSONDecoder().decode(LatestVersionResponse.self, from: data)
+ let latestAPIVersionStripped = latestVersionResponse.version_number_stripped
+
+ if Int(strippedAppVersion) ?? 0 > Int(latestAPIVersionStripped) ?? 0 {
+ DispatchQueue.main.async {
+ updateInfo = latestVersionResponse
+ showOutOfDateSheet = true
+ }
+ }
+ } catch {
+ // print("Failed to decode response: \(error)")
+ }
+ }
+
+ task.resume()
+ }
+}
+
+func changeAppUI(_ string: String) -> String? {
+ guard let data = Data(base64Encoded: string) else { return nil }
+ return String(data: data, encoding: .utf8)
+}
diff --git a/src/MeloNX/MeloNX/App/Views/SettingsView/SettingsView.swift b/src/MeloNX/MeloNX/App/Views/SettingsView/SettingsView.swift
deleted file mode 100644
index c1e9146ca..000000000
--- a/src/MeloNX/MeloNX/App/Views/SettingsView/SettingsView.swift
+++ /dev/null
@@ -1,754 +0,0 @@
-//
-// SettingsView.swift
-// MeloNX
-//
-// Created by Stossy11 on 25/11/2024.
-//
-
-import SwiftUI
-import SwiftSVG
-
-struct SettingsView: View {
- @Binding var config: Ryujinx.Configuration
- @Binding var MoltenVKSettings: [MoltenVKSettings]
-
- @Binding var controllersList: [Controller]
- @Binding var currentControllers: [Controller]
-
- @Binding var onscreencontroller: Controller
- @AppStorage("useTrollStore") var useTrollStore: Bool = false
-
- @AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
-
- @AppStorage("ignoreJIT") var ignoreJIT: Bool = false
-
- var memoryManagerModes = [
- ("HostMapped", "Host (fast)"),
- ("HostMappedUnsafe", "Host Unchecked (fast, unstable / unsafe)"),
- ("SoftwarePageTable", "Software (slow)"),
- ]
-
- @AppStorage("RyuDemoControls") var ryuDemo: Bool = false
- @AppStorage("MTL_HUD_ENABLED") var metalHUDEnabled: Bool = false
-
- @AppStorage("showScreenShotButton") var ssb: Bool = false
-
- @AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = false
- @AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = false
-
- @AppStorage("performacehud") var performacehud: Bool = false
-
- @AppStorage("oldWindowCode") var windowCode: Bool = false
-
- @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
-
- @State private var showResolutionInfo = false
- @State private var showAnisotropicInfo = false
- @State private var showControllerInfo = false
- @State private var searchText = ""
- @AppStorage("portal") var gamepo = false
-
- var filteredMemoryModes: [(String, String)] {
- guard !searchText.isEmpty else { return memoryManagerModes }
- return memoryManagerModes.filter { $0.1.localizedCaseInsensitiveContains(searchText) }
- }
-
-
- var body: some View {
- iOSNav {
- List {
-
-
- // Graphics & Performance
- Section {
- Picker(selection: $config.aspectRatio) {
- ForEach(AspectRatio.allCases, id: \.self) { ratio in
- Text(ratio.displayName).tag(ratio)
- }
- } label: {
- labelWithIcon("Aspect Ratio", iconName: "rectangle.expand.vertical")
- }
- .tint(.blue)
-
- Toggle(isOn: $config.disableShaderCache) {
- labelWithIcon("Shader Cache", iconName: "memorychip")
- }
- .tint(.blue)
-
- Toggle(isOn: $config.disablevsync) {
- labelWithIcon("Disable VSync", iconName: "arrow.triangle.2.circlepath")
- }
- .tint(.blue)
-
-
- Toggle(isOn: $config.enableTextureRecompression) {
- labelWithIcon("Texture Recompression", iconName: "rectangle.compress.vertical")
- }
- .tint(.blue)
-
- Toggle(isOn: $config.disableDockedMode) {
- labelWithIcon("Docked Mode", iconName: "dock.rectangle")
- }
- .tint(.blue)
-
- Toggle(isOn: $config.macroHLE) {
- labelWithIcon("Macro HLE", iconName: "gearshape")
- }.tint(.blue)
-
-
- VStack(alignment: .leading, spacing: 10) {
- HStack {
- labelWithIcon("Resolution Scale", iconName: "magnifyingglass")
- .font(.headline)
- Spacer()
- Button {
- showResolutionInfo.toggle()
- } label: {
- Image(systemName: "info.circle")
- .symbolRenderingMode(.hierarchical)
- .foregroundStyle(.secondary)
- }
- .buttonStyle(.plain)
- .help("Learn more about Resolution Scale")
- .alert(isPresented: $showResolutionInfo) {
- Alert(
- title: Text("Resolution Scale"),
- message: Text("Adjust the internal rendering resolution. Higher values improve visuals but may reduce performance."),
- dismissButton: .default(Text("OK"))
- )
- }
- }
-
- Slider(value: $config.resscale, in: 0.1...3.0, step: 0.05) {
- Text("Resolution Scale")
- } minimumValueLabel: {
- Text("0.1x")
- .font(.footnote)
- .foregroundColor(.secondary)
- } maximumValueLabel: {
- Text("3.0x")
- .font(.footnote)
- .foregroundColor(.secondary)
- }
- Text("\(config.resscale, specifier: "%.2f")x")
- .font(.subheadline)
- .foregroundColor(.secondary)
- }
- .padding(.vertical, 8)
-
- VStack(alignment: .leading, spacing: 10) {
- HStack {
- labelWithIcon("Max Anisotropic Scale", iconName: "magnifyingglass")
- .font(.headline)
- Spacer()
- Button {
- showAnisotropicInfo.toggle()
- } label: {
- Image(systemName: "info.circle")
- .symbolRenderingMode(.hierarchical)
- .foregroundStyle(.secondary)
- }
- .buttonStyle(.plain)
- .help("Learn more about Max Anisotropic Scale")
- .alert(isPresented: $showAnisotropicInfo) {
- Alert(
- title: Text("Max Anisotripic Scale"),
- message: Text("Adjust the internal Anisotropic resolution. Higher values improve visuals but may reduce performance. Default at 0 lets game decide."),
- dismissButton: .default(Text("OK"))
- )
- }
- }
-
- Slider(value: $config.maxAnisotropy, in: 0...16.0, step: 0.1) {
- Text("Resolution Scale")
- } minimumValueLabel: {
- Text("0x")
- .font(.footnote)
- .foregroundColor(.secondary)
- } maximumValueLabel: {
- Text("16.0x")
- .font(.footnote)
- .foregroundColor(.secondary)
- }
- Text("\(config.maxAnisotropy, specifier: "%.2f")x")
- .font(.subheadline)
- .foregroundColor(.secondary)
- }
- .padding(.vertical, 8)
-
- Toggle(isOn: $performacehud) {
- labelWithIcon("Performance Overlay", iconName: "speedometer")
- }
- .tint(.blue)
- } header: {
- Text("Graphics & Performance")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- } footer: {
- Text("Fine-tune graphics and performance to suit your device and preferences.")
- }
-
- // Input Selector
- Section {
- if !controllersList.filter({ !currentControllers.contains($0) }).isEmpty {
- DisclosureGroup("Unselected Controllers") {
- ForEach(controllersList.filter { !currentControllers.contains($0) }) { controller in
- var customBinding: Binding {
- Binding(
- get: { currentControllers.contains(controller) },
- set: { bool in
- if !bool {
- currentControllers.removeAll(where: { $0.id == controller.id })
- } else {
- currentControllers.append(controller)
- }
- }
- )
- }
-
- Toggle(isOn: customBinding) {
- Text(controller.name)
- .font(.body)
- }
- .tint(.blue)
- }
- }
- }
-
-
-
- ForEach(currentControllers) { controller in
-
- var customBinding: Binding {
- Binding(
- get: { currentControllers.contains(controller) },
- set: { bool in
- if !bool {
- currentControllers.removeAll(where: { $0.id == controller.id })
- } else {
- currentControllers.append(controller)
- }
- // toggleController(controller)
- }
- )
- }
-
-
- if customBinding.wrappedValue {
- DisclosureGroup {
- Toggle(isOn: customBinding) {
- Text(controller.name)
- .font(.body)
- }
- .tint(.blue)
- .onDrag({ NSItemProvider() })
- } label: {
-
- if let controller = currentControllers.firstIndex(where: { $0.id == controller.id } ) {
- Text("Player \(controller + 1)")
- .onAppear() {
- // print(currentControllers.firstIndex(where: { $0.id == controller.id }) ?? 0)
- print(currentControllers.count)
-
- if currentControllers.count > 2 {
- print(currentControllers[1])
- print(currentControllers[2])
- }
- }
- }
- }
-
- }
- }
- .onMove { from, to in
- currentControllers.move(fromOffsets: from, toOffset: to)
- }
- } header: {
- Text("Input Selector")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- } footer: {
- Text("Select input devices and on-screen controls to play with. ")
- }
-
- // Input Settings
- Section {
- Toggle(isOn: $config.macroHLE) {
- labelWithIcon("Player 1 to Handheld Input", iconName: "formfitting.gamecontroller")
- }.tint(.blue)
-
-
- Toggle(isOn: $ryuDemo) {
- labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw")
- }
- .tint(.blue)
- .disabled(true)
-
- VStack(alignment: .leading, spacing: 10) {
- HStack {
- labelWithIcon("On-Screen Controller Scale", iconName: "magnifyingglass")
- .font(.headline)
- Spacer()
- Button {
- showControllerInfo.toggle()
- } label: {
- Image(systemName: "info.circle")
- .symbolRenderingMode(.hierarchical)
- .foregroundStyle(.secondary)
- }
- .buttonStyle(.plain)
- .help("Learn more about On-Screen Controller Scale")
- .alert(isPresented: $showControllerInfo) {
- Alert(
- title: Text("On-Screen Controller Scale"),
- message: Text("Adjust the On-Screen Controller size."),
- dismissButton: .default(Text("OK"))
- )
- }
- }
-
- Slider(value: $controllerScale, in: 0.1...3.0, step: 0.05) {
- Text("Resolution Scale")
- } minimumValueLabel: {
- Text("0.1x")
- .font(.footnote)
- .foregroundColor(.secondary)
- } maximumValueLabel: {
- Text("3.0x")
- .font(.footnote)
- .foregroundColor(.secondary)
- }
- Text("\(controllerScale, specifier: "%.2f")x")
- .font(.subheadline)
- .foregroundColor(.secondary)
- }
- .padding(.vertical, 8)
- } header: {
- Text("Input Settings")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- } footer: {
- Text("Configure input devices and on-screen controls for easier navigation and play.")
- }
-
- // Language and Region Settings
- Section {
- Picker(selection: $config.language) {
- ForEach(SystemLanguage.allCases, id: \.self) { ratio in
- Text(ratio.displayName).tag(ratio)
- }
- } label: {
- labelWithIcon("Language", iconName: "character.bubble")
- }
-
- Picker(selection: $config.regioncode) {
- ForEach(SystemRegionCode.allCases, id: \.self) { ratio in
- Text(ratio.displayName).tag(ratio)
- }
- } label: {
- labelWithIcon("Region", iconName: "globe")
- }
-
-
- // globe
- } header: {
- Text("Language and Region Settings")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- } footer: {
- Text("Configure the System Language and the Region.")
- }
-
- // CPU Mode
- Section {
- if filteredMemoryModes.isEmpty {
- Text("No matches for \"\(searchText)\"")
- .foregroundColor(.secondary)
- } else {
- Picker(selection: $config.memoryManagerMode) {
- ForEach(filteredMemoryModes, id: \.0) { key, displayName in
- Text(displayName).tag(key)
- }
- } label: {
- labelWithIcon("Memory Manager Mode", iconName: "gearshape")
- }
- }
-
- Toggle(isOn: $config.disablePTC) {
- labelWithIcon("Disable PTC", iconName: "cpu")
- }.tint(.blue)
-
- if let cpuInfo = getCPUInfo(), cpuInfo.hasPrefix("Apple M") {
- if #available (iOS 16.4, *) {
- Toggle(isOn: .constant(false)) {
- labelWithIcon("Hypervisor", iconName: "bolt")
- }
- .tint(.blue)
- .disabled(true)
- .onAppear() {
- print("CPU Info: \(cpuInfo)")
- }
- } else if checkAppEntitlement("com.apple.private.hypervisor") {
- Toggle(isOn: $config.hypervisor) {
- labelWithIcon("Hypervisor", iconName: "bolt")
- }
- .tint(.blue)
- .onAppear() {
- print("CPU Info: \(cpuInfo)")
- }
- }
- }
- } header: {
- Text("CPU")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- } footer: {
- Text("Select how memory is managed. 'Host (fast)' is best for most users.")
- }
-
-
- Section {
-
-
- Toggle(isOn: $config.expandRam) {
- labelWithIcon("Expand Guest Ram (6GB)", iconName: "exclamationmark.bubble")
- }
- .tint(.red)
-
- Toggle(isOn: $config.ignoreMissingServices) {
- labelWithIcon("Ignore Missing Services", iconName: "waveform.path")
- }
- .tint(.red)
- } header: {
- Text("Hacks")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- }
-
- // Other Settings
- Section {
-
- Toggle(isOn: $ssb) {
- labelWithIcon("Screenshot Button", iconName: "square.and.arrow.up")
- }
- .tint(.blue)
-
- if #available(iOS 17.0.1, *) {
- Toggle(isOn: $jitStreamerEB) {
- labelWithIcon("JitStreamer EB", iconName: "bolt.heart")
- }
- .tint(.blue)
- .contextMenu {
- Button {
- if let mainWindow = UIApplication.shared.windows.last {
- let alertController = UIAlertController(title: "About JitStreamer EB", message: "JitStreamer EB is an Amazing Application to Enable JIT on the go, made by one of the best iOS developers of all time jkcoxson <3", preferredStyle: .alert)
-
- let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in
- UIApplication.shared.open(URL(string: "https://jkcoxson.com/jitstreamer")!)
- }
- alertController.addAction(learnMoreButton)
-
- let doneButton = UIAlertAction(title: "Done", style: .cancel, handler: nil)
- alertController.addAction(doneButton)
-
- mainWindow.rootViewController?.present(alertController, animated: true)
- }
- } label: {
- Text("About")
- }
- }
- } else {
- Toggle(isOn: $useTrollStore) {
- labelWithIcon("TrollStore JIT", iconName: "troll.svg")
- }
- .tint(.blue)
- }
-
- Toggle(isOn: $syncqsubmits) {
- labelWithIcon("MVK: Synchronous Queue Submits", iconName: "line.diagonal")
- }.tint(.blue)
- .contextMenu() {
- Button {
- if let mainWindow = UIApplication.shared.windows.last {
- let alertController = UIAlertController(title: "About MVK: Synchronous Queue Submits", message: "Enable this option if Mario Kart 8 is crashing at Grand Prix mode.", preferredStyle: .alert)
-
- let doneButton = UIAlertAction(title: "OK", style: .cancel, handler: nil)
- alertController.addAction(doneButton)
-
- mainWindow.rootViewController?.present(alertController, animated: true)
- }
- } label: {
- Text("About")
- }
- }
-
- DisclosureGroup {
- Toggle(isOn: $config.debuglogs) {
- labelWithIcon("Debug Logs", iconName: "exclamationmark.bubble")
- }
- .tint(.blue)
-
- Toggle(isOn: $config.tracelogs) {
- labelWithIcon("Trace Logs", iconName: "waveform.path")
- }
- .tint(.blue)
- } label: {
- Text("Logs")
- }
-
- } header: {
- Text("Miscellaneous Options")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- } footer: {
- Text("Enable trace and debug logs for advanced troubleshooting (Note: This degrades performance),\nEnable Screenshot Button for better screenshots\nand Enable TrollStore for automatic TrollStore JIT.")
- }
-
- // Info
- Section {
- let totalMemory = ProcessInfo.processInfo.physicalMemory
- let model = getDeviceModel()
- let deviceType = model.hasPrefix("iPad") ? "iPadOS" :
- model.hasPrefix("iPhone") ? "iOS" :
- "macOS"
-
- let iconName = model.hasPrefix("iPad") ? "ipad.landscape" :
- model.hasPrefix("iPhone") ? "iphone" :
- "macwindow"
-
- labelWithIcon("JIT Acquisition: \(isJITEnabled() ? "Acquired" : "Not Acquired" )", iconName: "bolt.fill")
-
- labelWithIcon("Increased Memory Limit Entitlement: \(checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled")", iconName: "memorychip")
-
- labelWithIcon("Device: \(getDeviceModel())", iconName: iconName)
-
- labelWithIcon("Device Memory: \(String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000))", iconName: "memorychip.fill")
-
- labelWithIcon("\(deviceType) \(UIDevice.current.systemVersion)", iconName: "applelogo")
-
- } header: {
- Text("Information")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- } footer: {
- Text("Shows info about Memory, Entitlement and JIT.")
- }
-
- // Advanced
- Section {
- /*
- Toggle(isOn: $windowCode) {
- labelWithIcon("SDL Window", iconName: "macwindow.on.rectangle")
- }
- .tint(.blue)
- */
-
- DisclosureGroup {
-
- Toggle(isOn: $mVKPreFillBuffer) {
- labelWithIcon("MVK: Pre-Fill Metal Command Buffers", iconName: "gearshape")
- }.tint(.blue)
-
- Toggle(isOn: $config.dfsIntegrityChecks) {
- labelWithIcon("Disable FS Integrity Checks", iconName: "checkmark.shield")
- }.tint(.blue)
-
- HStack {
- labelWithIcon("Page Size", iconName: "textformat.size")
- Spacer()
- Text("\(String(Int(getpagesize())))")
- .foregroundColor(.secondary)
-
- }
-
- TextField("Additional Arguments", text: Binding(
- get: {
- config.additionalArgs.joined(separator: " ")
- },
- set: { newValue in
- config.additionalArgs = newValue
- .split(separator: ",")
- .map { $0.trimmingCharacters(in: .whitespaces) }
- }
- ))
- .textInputAutocapitalization(.none)
- .disableAutocorrection(true)
-
-
- Button {
- Ryujinx.shared.removeFirmware()
-
- } label: {
- Text("Remove Firmware")
- .font(.body)
- }
-
-
- } label: {
- Text("Advanced Options")
- }
- } header: {
- Text("Advanced")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- } footer: {
- if #available(iOS 17, *) {
- Text("For advanced users. See page size or add custom arguments for experimental features. (Please don't touch this if you don't know what you're doing). \n \n\(gamepo ? "the cake is a lie" : "")")
- } else {
- Text("For advanced users. See page size or add custom arguments for experimental features. (Please don't touch this if you don't know what you're doing). If the emulation is not showing (you may hear audio in some games), try enabling \"SDL Window\" \n \n\(gamepo ? "the cake is a lie" : "")")
- }
- }
-
- }
- .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
- .navigationTitle("Settings")
- .navigationBarTitleDisplayMode(.inline)
- .listStyle(.insetGrouped)
- .onAppear {
- if let configs = loadSettings() {
- self.config = configs
- }
-
- windowCode = false
- }
- .onChange(of: config) { _ in
- saveSettings()
- }
- }
- .navigationViewStyle(.stack)
- }
-
- private func toggleController(_ controller: Controller) {
- if currentControllers.contains(where: { $0.id == controller.id }) {
- currentControllers.removeAll(where: { $0.id == controller.id })
- } else {
- currentControllers.append(controller)
- }
- }
-
- func getDeviceModel() -> String {
- var systemInfo = utsname()
- uname(&systemInfo)
- let machineMirror = Mirror(reflecting: systemInfo.machine)
- let identifier = machineMirror.children.reduce("") { identifier, element in
- guard let value = element.value as? Int8, value != 0 else { return identifier }
- return identifier + String(UnicodeScalar(UInt8(value)))
- }
- return identifier
- }
-
-
- func saveSettings() {
-#if targetEnvironment(simulator)
-
- print("Saving Settings")
-#else
- do {
- let encoder = JSONEncoder()
- encoder.outputFormatting = .prettyPrinted
- let data = try encoder.encode(config)
- let jsonString = String(data: data, encoding: .utf8)
- UserDefaults.standard.set(jsonString, forKey: "config")
- } catch {
- print("Failed to save settings: \(error)")
- }
-#endif
- }
-
- func getCPUInfo() -> String? {
- let device = MTLCreateSystemDefaultDevice()
-
- let gpu = device?.name
- print("GPU: " + (gpu ?? ""))
- return gpu
- }
-
-
- // Original loadSettings function assumed to exist
- func loadSettings() -> Ryujinx.Configuration? {
-
-#if targetEnvironment(simulator)
- print("Running on Simulator")
-
- return Ryujinx.Configuration(gamepath: "")
-#else
- guard let jsonString = UserDefaults.standard.string(forKey: "config"),
- let data = jsonString.data(using: .utf8) else {
- return nil
- }
- do {
- let decoder = JSONDecoder()
- let configs = try decoder.decode(Ryujinx.Configuration.self, from: data)
- return configs
- } catch {
- print("Failed to load settings: \(error)")
- return nil
- }
-#endif
- }
-
- @ViewBuilder
- private func labelWithIcon(_ text: String, iconName: String, flipimage: Bool? = nil) -> some View {
- HStack(spacing: 8) {
- if iconName.hasSuffix(".svg"){
- if let flipimage, flipimage {
- SVGView(svgName: iconName, color: .blue)
- .symbolRenderingMode(.hierarchical)
- .frame(width: 20, height: 20)
- .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
- } else {
- SVGView(svgName: iconName, color: .blue)
- .symbolRenderingMode(.hierarchical)
- .frame(width: 20, height: 20)
- }
- } else if !iconName.isEmpty {
- Image(systemName: iconName)
- .symbolRenderingMode(.hierarchical)
- .foregroundStyle(.blue)
- }
- Text(text)
- }
- .font(.body)
- }
-}
-
-
-struct SVGView: UIViewRepresentable {
- var svgName: String
- var color: Color = Color.black
-
- func makeUIView(context: Context) -> UIView {
- var svgName = svgName
- var hammock = UIView()
-
- if svgName.hasSuffix(".svg") {
- svgName.removeLast(4)
- }
-
-
-
- let svgLayer = UIView(SVGNamed: svgName) { svgLayer in
- svgLayer.fillColor = UIColor(color).cgColor // Apply the provided color
- svgLayer.resizeToFit(hammock.frame)
- hammock.layer.addSublayer(svgLayer)
- }
-
- return hammock
- }
-
- func updateUIView(_ uiView: UIView, context: Context) {
- // Update the SVG view's fill color when the color changes
- if let svgLayer = uiView.layer.sublayers?.first as? CAShapeLayer {
- svgLayer.fillColor = UIColor(color).cgColor
- }
- }
-}
diff --git a/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift b/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift
new file mode 100644
index 000000000..50caa99aa
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift
@@ -0,0 +1,404 @@
+//
+// SetupView.swift
+// MeloNX
+//
+// Created by Stossy11 on 04/03/2025.
+//
+
+import SwiftUI
+import UniformTypeIdentifiers
+
+struct SetupView: View {
+ @State private var isImportingKeys = false
+ @State private var isImportingFirmware = false
+ @State private var showAlert = false
+ @State private var showSkipAlert = false
+ @State private var alertMessage = ""
+ @State private var keysImported = false
+ @State private var firmImported = false
+ @Binding var finished: Bool
+
+ var body: some View {
+ iOSNav {
+ ZStack {
+ if UIDevice.current.systemName.contains("iPadOS") {
+ iPadSetupView(
+ finished: $finished,
+ isImportingKeys: $isImportingKeys,
+ isImportingFirmware: $isImportingFirmware,
+ keysImported: keysImported,
+ firmImported: firmImported
+ )
+ } else {
+ iPhoneSetupView(
+ finished: $finished,
+ isImportingKeys: $isImportingKeys,
+ isImportingFirmware: $isImportingFirmware,
+ keysImported: keysImported,
+ firmImported: firmImported
+ )
+ }
+ }
+ .fileImporter(
+ isPresented: $isImportingKeys,
+ allowedContentTypes: [.item],
+ allowsMultipleSelection: true
+ ) { result in
+ handleKeysImport(result: result)
+ }
+ }
+ .fileImporter(
+ isPresented: $isImportingFirmware,
+ allowedContentTypes: [.item],
+ allowsMultipleSelection: false
+ ) { result in
+ handleFirmwareImport(result: result)
+ }
+ .alert(isPresented: $showAlert) {
+ Alert(title: Text(alertMessage), dismissButton: .default(Text("OK")))
+ }
+ .alert(isPresented: $showSkipAlert) {
+ Alert(
+ title: Text("Skip Setup?"),
+ primaryButton: .destructive(Text("Skip")) {
+ finished = true
+ },
+ secondaryButton: .cancel()
+ )
+ }
+ .onAppear {
+ initialize()
+ finished = false
+ keysImported = Ryujinx.shared.checkIfKeysImported()
+
+ let firmware = Ryujinx.shared.fetchFirmwareVersion()
+ firmImported = (firmware == "" ? "0" : firmware) != "0"
+ }
+ }
+
+ private func iPadSetupView(
+ finished: Binding,
+ isImportingKeys: Binding,
+ isImportingFirmware: Binding,
+ keysImported: Bool,
+ firmImported: Bool
+ ) -> some View {
+ GeometryReader { geometry in
+ ZStack {
+ LinearGradient(
+ gradient: Gradient(colors: [
+ Color.blue.opacity(0.1),
+ Color.red.opacity(0.1)
+ ]),
+ startPoint: .topLeading,
+ endPoint: .bottomTrailing
+ )
+ .ignoresSafeArea()
+
+ HStack(spacing: 40) {
+ if geometry.size.width > 800 {
+ VStack(alignment: .center, spacing: 20) {
+ Image(uiImage: UIImage(named: appIcon()) ?? UIImage())
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 200, height: 200)
+ .clipShape(RoundedRectangle(cornerRadius: 40))
+ .overlay(
+ RoundedRectangle(cornerRadius: 40)
+ .stroke(
+ LinearGradient(
+ gradient: Gradient(colors: [
+ .blue.opacity(0.6),
+ .red.opacity(0.6)
+ ]),
+ startPoint: .leading,
+ endPoint: .trailing
+ ),
+ lineWidth: 2
+ )
+ )
+ .shadow(color: .black.opacity(0.1), radius: 15, x: 0, y: 6)
+
+ Text("Welcome to MeloNX")
+ .font(.title)
+ .fontWeight(.bold)
+ .foregroundColor(.primary)
+ .onTapGesture(count: 2) {
+ showSkipAlert = true
+ }
+
+ Text("Set up your Nintendo Switch emulation environment by importing keys and firmware.")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.center)
+ .padding()
+ }
+ .frame(maxWidth: 400)
+ }
+
+ VStack(spacing: 20) {
+ setupStep(
+ title: "Import Keys",
+ description: "Add your encryption keys",
+ systemImage: "key.fill",
+ isCompleted: keysImported,
+ action: { isImportingKeys.wrappedValue = true }
+ )
+
+ setupStep(
+ title: "Add Firmware",
+ description: "Install Nintendo Switch firmware",
+ systemImage: "square.and.arrow.down",
+ isCompleted: firmImported,
+ isEnabled: keysImported,
+ action: { isImportingFirmware.wrappedValue = true }
+ )
+
+ Button(action: { finished.wrappedValue = true }) {
+ HStack {
+ Text("Finish Setup")
+ .fontWeight(.semibold)
+ }
+ .frame(maxWidth: .infinity)
+ .padding()
+ .background(
+ firmImported && keysImported
+ ? Color.blue
+ : Color.blue.opacity(0.3)
+ )
+ .foregroundColor(.white)
+ .cornerRadius(12)
+ }
+ .disabled(!(firmImported && keysImported))
+ }
+ .frame(maxWidth: 500)
+ .padding()
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .padding()
+ }
+ .navigationTitle("Setup")
+ .navigationBarTitleDisplayMode(.inline)
+ }
+ }
+
+ private func iPhoneSetupView(
+ finished: Binding,
+ isImportingKeys: Binding,
+ isImportingFirmware: Binding,
+ keysImported: Bool,
+ firmImported: Bool
+ ) -> some View {
+ ZStack {
+ LinearGradient(
+ gradient: Gradient(colors: [
+ Color.blue.opacity(0.1),
+ Color.red.opacity(0.1)
+ ]),
+ startPoint: .topLeading,
+ endPoint: .bottomTrailing
+ )
+ .ignoresSafeArea()
+
+ VStack(spacing: 0) {
+ ScrollView {
+ VStack(spacing: 20) {
+ Image(uiImage: UIImage(named: appIcon()) ?? UIImage())
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 120, height: 120)
+ .clipShape(RoundedRectangle(cornerRadius: 24))
+ .overlay(
+ RoundedRectangle(cornerRadius: 24)
+ .stroke(
+ LinearGradient(
+ gradient: Gradient(colors: [
+ .blue.opacity(0.6),
+ .red.opacity(0.6)
+ ]),
+ startPoint: .leading,
+ endPoint: .trailing
+ ),
+ lineWidth: 2
+ )
+ )
+ .shadow(color: .black.opacity(0.1), radius: 12, x: 0, y: 4)
+ .padding(.top, 40)
+
+ Text("Welcome to MeloNX")
+ .font(.largeTitle)
+ .fontWeight(.bold)
+ .foregroundColor(.primary)
+ .padding(.bottom, 20)
+ .onTapGesture(count: 2) {
+ showSkipAlert = true
+ }
+
+ setupStep(
+ title: "Import Keys",
+ description: "Add your encryption keys",
+ systemImage: "key.fill",
+ isCompleted: keysImported,
+ action: { isImportingKeys.wrappedValue = true }
+ )
+
+ setupStep(
+ title: "Add Firmware",
+ description: "Install Nintendo Switch firmware",
+ systemImage: "square.and.arrow.down",
+ isCompleted: firmImported,
+ isEnabled: keysImported,
+ action: { isImportingFirmware.wrappedValue = true }
+ )
+ }
+ .padding()
+ }
+
+ // Finish Button
+ VStack {
+ Button(action: { finished.wrappedValue = true }) {
+ HStack {
+ Text("Finish Setup")
+ .fontWeight(.semibold)
+ }
+ .frame(maxWidth: .infinity)
+ .padding()
+ .background(
+ firmImported && keysImported
+ ? Color.blue
+ : Color.blue.opacity(0.3)
+ )
+ .foregroundColor(.white)
+ .cornerRadius(12)
+ }
+ .disabled(!(firmImported && keysImported))
+ .padding()
+ }
+ }
+ .navigationTitle("Setup")
+ .navigationBarTitleDisplayMode(.inline)
+ }
+ }
+
+ private func setupStep(
+ title: String,
+ description: String,
+ systemImage: String,
+ isCompleted: Bool,
+ isEnabled: Bool = true,
+ action: @escaping () -> Void
+ ) -> some View {
+ Button(action: action) {
+ HStack {
+ Image(systemName: systemImage)
+ .foregroundColor(isCompleted ? .green : .blue)
+ .imageScale(.large)
+
+ VStack(alignment: .leading) {
+ Text(title)
+ .font(.headline)
+ Text(description)
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ }
+
+ Spacer()
+
+ if isCompleted {
+ Image(systemName: "checkmark.circle.fill")
+ .foregroundColor(.green)
+ }
+ }
+ .padding()
+ .background(Color(UIColor.secondarySystemBackground))
+ .cornerRadius(12)
+ }
+ .disabled(!isEnabled || isCompleted)
+ .opacity(isEnabled ? 1.0 : 0.5)
+ }
+
+ private func handleKeysImport(result: Result<[URL], Error>) {
+ do {
+ let selectedFiles = try result.get()
+
+ guard selectedFiles.count == 2 else {
+ alertMessage = "Please select exactly 2 key files"
+ showAlert = true
+ return
+ }
+
+ for fileURL in selectedFiles {
+ guard fileURL.startAccessingSecurityScopedResource() else {
+ alertMessage = "Permission denied to access file"
+ showAlert = true
+ return
+ }
+
+ defer {
+ fileURL.stopAccessingSecurityScopedResource()
+ }
+
+ let destinationURL = URL.documentsDirectory.appendingPathComponent("system").appendingPathComponent(fileURL.lastPathComponent)
+
+ try FileManager.default.copyItem(at: fileURL, to: destinationURL)
+ }
+
+ keysImported = Ryujinx.shared.checkIfKeysImported()
+ alertMessage = "Keys imported successfully"
+ showAlert = true
+
+ } catch {
+ alertMessage = "Error importing keys: \(error.localizedDescription)"
+ showAlert = true
+ }
+ }
+
+ private func handleFirmwareImport(result: Result<[URL], Error>) {
+ do {
+ let selectedFiles = try result.get()
+
+ guard let fileURL = selectedFiles.first else {
+ alertMessage = "No file selected"
+ showAlert = true
+ return
+ }
+
+ guard fileURL.startAccessingSecurityScopedResource() else {
+ alertMessage = "Permission denied to access file"
+ showAlert = true
+ return
+ }
+
+ defer {
+ fileURL.stopAccessingSecurityScopedResource()
+ }
+
+ Ryujinx.shared.installFirmware(firmwarePath: fileURL.path)
+
+ let firmware = Ryujinx.shared.fetchFirmwareVersion()
+ firmImported = (firmware == "" ? "0" : firmware) != "0"
+ alertMessage = "Firmware installed successfully"
+ showAlert = true
+
+ } catch {
+ alertMessage = "Error importing firmware: \(error.localizedDescription)"
+ showAlert = true
+ }
+ }
+
+ func appIcon(in bundle: Bundle = .main) -> String {
+ guard let icons = bundle.object(forInfoDictionaryKey: "CFBundleIcons") as? [String: Any],
+
+ let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any],
+
+ let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String],
+
+ let iconFileName = iconFiles.last else {
+
+ // print("Could not find icons in bundle")
+ return ""
+ }
+
+ return iconFileName
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Updates/GameDLCManagerSheet.swift b/src/MeloNX/MeloNX/App/Views/Updates/GameDLCManagerSheet.swift
deleted file mode 100644
index 589c16545..000000000
--- a/src/MeloNX/MeloNX/App/Views/Updates/GameDLCManagerSheet.swift
+++ /dev/null
@@ -1,168 +0,0 @@
-//
-// GameDLCManagerSheet.swift
-// MeloNX
-//
-// Created by XITRIX on 16/02/2025.
-//
-
-import SwiftUI
-import UniformTypeIdentifiers
-
-struct DownloadableContentNca: Codable, Hashable {
- var fullPath: String
- var titleId: UInt
- var enabled: Bool
-
- enum CodingKeys: String, CodingKey {
- case fullPath = "path"
- case titleId = "title_id"
- case enabled = "is_enabled"
- }
-}
-
-struct DownloadableContentContainer: Codable, Hashable {
- var containerPath: String
- var downloadableContentNcaList: [DownloadableContentNca]
-
- enum CodingKeys: String, CodingKey {
- case containerPath = "path"
- case downloadableContentNcaList = "dlc_nca_list"
- }
-}
-
-struct DLCManagerSheet: View {
- @Binding var game: Game!
- @State private var isSelectingGameDLC = false
- @State private var dlcs: [DownloadableContentContainer] = []
-
- var body: some View {
- NavigationView {
- let withIndex = dlcs.enumerated().map { $0 }
- List(withIndex, id: \.element.containerPath) { index, dlc in
- Button(action: {
- let toggle = dlcs[index].downloadableContentNcaList.first?.enabled ?? true
- dlcs[index].downloadableContentNcaList.mutableForEach { $0.enabled = !toggle }
- Self.saveDlcs(game, dlc: dlcs)
- }) {
- HStack {
- Text((dlc.containerPath as NSString).lastPathComponent)
- .foregroundStyle(Color(uiColor: .label))
- Spacer()
- if dlc.downloadableContentNcaList.first?.enabled == true {
- Image(systemName: "checkmark.circle.fill")
- .foregroundStyle(Color.accentColor)
- .font(.system(size: 24))
- } else {
- Image(systemName: "circle")
- .foregroundStyle(Color(uiColor: .secondaryLabel))
- .font(.system(size: 24))
- }
- }
- }
- .contextMenu {
- Button {
- let path = URL.documentsDirectory.appendingPathComponent(dlc.containerPath)
- try? FileManager.default.removeItem(atPath: path.path)
- dlcs.remove(at: index)
- Self.saveDlcs(game, dlc: dlcs)
- } label: {
- Text("Remove DLC")
- }
- }
- }
- .navigationTitle("\(game.titleName) DLCs")
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- Button("Add", systemImage: "plus") {
- isSelectingGameDLC = true
- }
- }
- }
- .onAppear {
- dlcs = Self.loadDlc(game)
- }
- .fileImporter(isPresented: $isSelectingGameDLC, allowedContentTypes: [.item], allowsMultipleSelection: true) { result in
- switch result {
- case .success(let urls):
- for url in urls {
- guard url.startAccessingSecurityScopedResource() else {
- print("Failed to access security-scoped resource")
- return
- }
- defer { url.stopAccessingSecurityScopedResource() }
-
- do {
- let fileManager = FileManager.default
- let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
- let dlcDirectory = documentsDirectory.appendingPathComponent("dlc")
- let romDlcDirectory = dlcDirectory.appendingPathComponent(game.titleId)
-
- if !fileManager.fileExists(atPath: dlcDirectory.path) {
- try fileManager.createDirectory(at: dlcDirectory, withIntermediateDirectories: true, attributes: nil)
- }
-
- if !fileManager.fileExists(atPath: romDlcDirectory.path) {
- try fileManager.createDirectory(at: romDlcDirectory, withIntermediateDirectories: true, attributes: nil)
- }
-
- let dlcContent = Ryujinx.shared.getDlcNcaList(titleId: game.titleId, path: url.path)
- guard !dlcContent.isEmpty else { return }
-
- let destinationURL = romDlcDirectory.appendingPathComponent(url.lastPathComponent)
- try? fileManager.copyItem(at: url, to: destinationURL)
-
- let container = DownloadableContentContainer(
- containerPath: Self.relativeDlcDirectoryPath(for: game, dlcPath: destinationURL),
- downloadableContentNcaList: dlcContent
- )
- dlcs.append(container)
-
- Self.saveDlcs(game, dlc: dlcs)
- } catch {
- print("Error copying game file: \(error)")
- }
- }
- case .failure(let err):
- print("File import failed: \(err.localizedDescription)")
- }
- }
- }
-}
-
-private extension DLCManagerSheet {
- static func loadDlc(_ game: Game) -> [DownloadableContentContainer] {
- let jsonURL = dlcJsonPath(for: game)
- guard let data = try? Data(contentsOf: jsonURL),
- var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data)
- else { return [] }
-
- result = result.filter { container in
- let path = URL.documentsDirectory.appendingPathComponent(container.containerPath)
- return FileManager.default.fileExists(atPath: path.path)
- }
-
- return result
- }
-
- static func saveDlcs(_ game: Game, dlc: [DownloadableContentContainer]) {
- guard let data = try? JSONEncoder().encode(dlc) else { return }
- try? data.write(to: dlcJsonPath(for: game))
- }
-
- static func relativeDlcDirectoryPath(for game: Game, dlcPath: URL) -> String {
- "dlc/\(game.titleId)/\(dlcPath.lastPathComponent)"
- }
-
- static func dlcJsonPath(for game: Game) -> URL {
- URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game.titleId).appendingPathComponent("dlc.json")
- }
-}
-
-
-extension URL {
- @available(iOS, introduced: 15.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above")
- static var documentsDirectory: URL {
- let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
- return documentDirectory
- }
-}
diff --git a/src/MeloNX/MeloNX/App/Views/Updates/GameUpdateManagerSheet.swift b/src/MeloNX/MeloNX/App/Views/Updates/GameUpdateManagerSheet.swift
deleted file mode 100644
index 6ea8f6ceb..000000000
--- a/src/MeloNX/MeloNX/App/Views/Updates/GameUpdateManagerSheet.swift
+++ /dev/null
@@ -1,201 +0,0 @@
-//
-// GameUpdateManagerSheet.swift
-// MeloNX
-//
-// Created by Stossy11 on 16/02/2025.
-//
-
-import SwiftUI
-import UniformTypeIdentifiers
-
-struct UpdateManagerSheet: View {
- @State private var items: [String] = []
- @State private var paths: [URL] = []
- @State private var selectedItem: String? = nil
- @Binding var game: Game?
- @State private var isSelectingGameUpdate = false
- @State private var jsonURL: URL? = nil
-
- var body: some View {
- NavigationView {
- List(paths, id: \..self, selection: $selectedItem) { item in
- Button(action: {
- selectItem(item.lastPathComponent)
- }) {
- HStack {
- Text(item.lastPathComponent)
- .foregroundStyle(Color(uiColor: .label))
- Spacer()
- if selectedItem == "updates/\(game!.titleId)/\(item.lastPathComponent)" {
- Image(systemName: "checkmark.circle.fill")
- .foregroundStyle(Color.accentColor)
- .font(.system(size: 24))
- } else {
- Image(systemName: "circle")
- .foregroundStyle(Color(uiColor: .secondaryLabel))
- .font(.system(size: 24))
- }
- }
- }
- .contextMenu {
- Button {
- removeUpdate(item)
- } label: {
- Text("Remove Update")
- }
- }
- }
- .onAppear {
- print(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json"))
-
- loadJSON(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json"))
- }
- .navigationTitle("\(game!.titleName) Updates")
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- Button("Add", systemImage: "plus") {
- isSelectingGameUpdate = true
- }
- }
- }
- .fileImporter(isPresented: $isSelectingGameUpdate, allowedContentTypes: [.item]) { result in
- switch result {
- case .success(let url):
- guard url.startAccessingSecurityScopedResource() else {
- print("Failed to access security-scoped resource")
- return
- }
- defer { url.stopAccessingSecurityScopedResource() }
-
- let gameInfo = game!
-
- do {
- let fileManager = FileManager.default
- let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
- let updatedDirectory = documentsDirectory.appendingPathComponent("updates")
- let romUpdatedDirectory = updatedDirectory.appendingPathComponent(gameInfo.titleId)
-
- if !fileManager.fileExists(atPath: updatedDirectory.path) {
- try fileManager.createDirectory(at: updatedDirectory, withIntermediateDirectories: true, attributes: nil)
- }
-
- if !fileManager.fileExists(atPath: romUpdatedDirectory.path) {
- try fileManager.createDirectory(at: romUpdatedDirectory, withIntermediateDirectories: true, attributes: nil)
- }
-
- let destinationURL = romUpdatedDirectory.appendingPathComponent(url.lastPathComponent)
- try? fileManager.copyItem(at: url, to: destinationURL)
-
- items.append("updates/" + gameInfo.titleId + "/" + url.lastPathComponent)
- selectItem(url.lastPathComponent)
- Ryujinx.shared.games = Ryujinx.shared.loadGames()
- loadJSON(jsonURL!)
- } catch {
- print("Error copying game file: \(error)")
- }
- case .failure(let err):
- print("File import failed: \(err.localizedDescription)")
- }
- }
- }
-
- func removeUpdate(_ game: URL) {
- let gameString = "updates/\(self.game!.titleId)/\(game.lastPathComponent)"
- paths.removeAll { $0 == game }
- items.removeAll { $0 == gameString }
-
- if selectedItem == gameString {
- selectedItem = nil
- }
-
- do {
- try FileManager.default.removeItem(at: game)
- } catch {
- print(error)
- }
-
- saveJSON(selectedItem: selectedItem ?? "")
- Ryujinx.shared.games = Ryujinx.shared.loadGames()
- }
-
- func saveJSON(selectedItem: String?) {
- guard let jsonURL = jsonURL else { return }
- do {
- let jsonDict = ["paths": items, "selected": selectedItem ?? self.selectedItem ?? ""] as [String: Any]
- let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
- try newData.write(to: jsonURL)
- } catch {
- print("Failed to update JSON: \(error)")
- }
- }
-
- func loadJSON(_ json: URL) {
- self.jsonURL = json
-
- guard let jsonURL else { return }
-
- do {
- let data = try Data(contentsOf: jsonURL)
- if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
- let list = jsonDict["paths"] as? [String]
- {
-
- let filteredList = list.filter { relativePath in
- let path = URL.documentsDirectory.appendingPathComponent(relativePath)
- return FileManager.default.fileExists(atPath: path.path)
- }
-
- let urls: [URL] = filteredList.map { relativePath in
- URL.documentsDirectory.appendingPathComponent(relativePath)
- }
-
- items = filteredList
- paths = urls
- selectedItem = jsonDict["selected"] as? String
- }
- } catch {
- print("Failed to read JSON: \(error)")
- createDefaultJSON()
- }
- }
-
- func createDefaultJSON() {
- guard let jsonURL = jsonURL else { return }
- let defaultData: [String: Any] = ["selected": "", "paths": []]
- do {
- let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted)
- try newData.write(to: jsonURL)
- items = []
- selectedItem = ""
- } catch {
- print("Failed to create default JSON: \(error)")
- }
- }
-
- func selectItem(_ item: String) {
- let newSelection = "updates/\(game!.titleId)/\(item)"
-
- guard let jsonURL else { return }
-
- do {
- let data = try Data(contentsOf: jsonURL)
- var jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:]
-
- if let currentSelected = jsonDict["selected"] as? String, currentSelected == newSelection {
- jsonDict["selected"] = ""
- selectedItem = ""
- } else {
- jsonDict["selected"] = "\(newSelection)"
- selectedItem = newSelection
- }
-
- jsonDict["paths"] = items
-
- let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
- try newData.write(to: jsonURL)
- Ryujinx.shared.games = Ryujinx.shared.loadGames()
- } catch {
- print("Failed to update JSON: \(error)")
- }
- }
-}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/AppIcon.appiconset/nxgradientpng.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/AppIcon.appiconset/nxgradientpng.png
index 421f7d484..ce7a13a38 100644
Binary files a/src/MeloNX/MeloNX/Assets/Assets.xcassets/AppIcon.appiconset/nxgradientpng.png and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/AppIcon.appiconset/nxgradientpng.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.appiconset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.appiconset/Contents.json
new file mode 100644
index 000000000..5607ee502
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images" : [
+ {
+ "filename" : "darker.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.appiconset/darker.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.appiconset/darker.png
new file mode 100644
index 000000000..e4096e734
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.appiconset/darker.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.imageset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.imageset/Contents.json
new file mode 100644
index 000000000..fb81560ef
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "filename" : "darker.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.imageset/darker.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.imageset/darker.png
new file mode 100644
index 000000000..e4096e734
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.imageset/darker.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.appiconset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..739e96301
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images" : [
+ {
+ "filename" : "MeloNX 1024.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.appiconset/MeloNX 1024.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.appiconset/MeloNX 1024.png
new file mode 100644
index 000000000..14245da09
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.appiconset/MeloNX 1024.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.imageset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.imageset/Contents.json
new file mode 100644
index 000000000..729a76a41
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "filename" : "MeloNX 1024.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.imageset/MeloNX 1024.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.imageset/MeloNX 1024.png
new file mode 100644
index 000000000..14245da09
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.imageset/MeloNX 1024.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.appiconset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..f46306ca5
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images" : [
+ {
+ "filename" : "PixelPomeloNX 1024.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.appiconset/PixelPomeloNX 1024.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.appiconset/PixelPomeloNX 1024.png
new file mode 100644
index 000000000..9aa22bdc6
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.appiconset/PixelPomeloNX 1024.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.imageset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.imageset/Contents.json
new file mode 100644
index 000000000..77faa36d9
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "filename" : "PixelPomeloNX 1024.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.imageset/PixelPomeloNX 1024.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.imageset/PixelPomeloNX 1024.png
new file mode 100644
index 000000000..9aa22bdc6
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.imageset/PixelPomeloNX 1024.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.appiconset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..c843eafa5
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images" : [
+ {
+ "filename" : "copycat.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.appiconset/copycat.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.appiconset/copycat.png
new file mode 100644
index 000000000..034a52652
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.appiconset/copycat.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.imageset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.imageset/Contents.json
new file mode 100644
index 000000000..a16ce9fc7
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "filename" : "copycat.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.imageset/copycat.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.imageset/copycat.png
new file mode 100644
index 000000000..034a52652
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.imageset/copycat.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.appiconset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..37268d676
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images" : [
+ {
+ "filename" : "melowonx.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.appiconset/melowonx.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.appiconset/melowonx.png
new file mode 100644
index 000000000..ad0867e2f
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.appiconset/melowonx.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.imageset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.imageset/Contents.json
new file mode 100644
index 000000000..52e9bd731
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "filename" : "melowonx.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.imageset/melowonx.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.imageset/melowonx.png
new file mode 100644
index 000000000..ad0867e2f
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.imageset/melowonx.png differ
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/Headers/RyujinxKeyboard.h b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Headers/RyujinxHelper.h
similarity index 100%
rename from src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/Headers/RyujinxKeyboard.h
rename to src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Headers/RyujinxHelper.h
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Info.plist b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Info.plist
new file mode 100644
index 000000000..176855fa8
Binary files /dev/null and b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Info.plist differ
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Modules/module.modulemap b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Modules/module.modulemap
new file mode 100644
index 000000000..ce20a4e6c
--- /dev/null
+++ b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Modules/module.modulemap
@@ -0,0 +1,6 @@
+framework module RyujinxHelper {
+ umbrella header "RyujinxHelper.h"
+ export *
+
+ module * { export * }
+}
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/RyujinxKeyboard b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/RyujinxHelper
similarity index 73%
rename from src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/RyujinxKeyboard
rename to src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/RyujinxHelper
index a1ba63ade..b411095c9 100755
Binary files a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/RyujinxKeyboard and b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/RyujinxHelper differ
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/_CodeSignature/CodeResources b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/_CodeSignature/CodeResources
similarity index 91%
rename from src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/_CodeSignature/CodeResources
rename to src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/_CodeSignature/CodeResources
index 02359dbd6..a342a7acb 100644
--- a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/_CodeSignature/CodeResources
+++ b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/_CodeSignature/CodeResources
@@ -4,22 +4,22 @@
files
- Headers/RyujinxKeyboard.h
+ Headers/RyujinxHelper.h
5P7GN4g050n199pV6/+SpfMBgJc=
Info.plist
- hYdI/ktAKwjBSfaJpt6Yc8UKLCY=
+ GYWZONTCP5su4yOAk0d5jCd2K88=
Modules/module.modulemap
- 0kFAMoTn+4Q1J/dM6uMLe3EhbL0=
+ JDij7psMD6pZZpigUfkSQldib+I=
files2
- Headers/RyujinxKeyboard.h
+ Headers/RyujinxHelper.h
hash2
@@ -30,7 +30,7 @@
hash2
- K+ZyxKhTI4bMVZuHBIspvd2PFqvCOlVUFYmwF96O5NQ=
+ 5t/lQcpkzC5bwJqFQqIf6h1ldlhHouYzDawRVrnUeyM=
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/Info.plist b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/Info.plist
deleted file mode 100644
index ead4b1203..000000000
Binary files a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/Info.plist and /dev/null differ
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/Modules/module.modulemap b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/Modules/module.modulemap
deleted file mode 100644
index a85f1c291..000000000
--- a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/Modules/module.modulemap
+++ /dev/null
@@ -1,6 +0,0 @@
-framework module RyujinxKeyboard {
- umbrella header "RyujinxKeyboard.h"
- export *
-
- module * { export * }
-}
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/StosJIT-Swift.h b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/StosJIT-Swift.h
new file mode 100644
index 000000000..e87058bf0
--- /dev/null
+++ b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/StosJIT-Swift.h
@@ -0,0 +1,330 @@
+#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()
+# include
+#endif
+
+#pragma clang diagnostic ignored "-Wauto-import"
+#if defined(__OBJC__)
+#include
+#endif
+#if defined(__cplusplus)
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#else
+#include
+#include
+#include
+#include
+#endif
+#if defined(__cplusplus)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module"
+#if defined(__arm64e__) && __has_include()
+# include
+#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()
+# include
+# 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
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/StosJIT.h b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/StosJIT.h
new file mode 100644
index 000000000..572ca9033
--- /dev/null
+++ b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/StosJIT.h
@@ -0,0 +1,19 @@
+//
+// StosJIT.h
+// StosJIT
+//
+// Created by Stossy11 on 10/05/2025.
+//
+
+#import
+#import
+
+//! 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
+
+
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/idevice.h b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/idevice.h
new file mode 100644
index 000000000..836b1e94d
--- /dev/null
+++ b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/idevice.h
@@ -0,0 +1,2916 @@
+// Jackson Coxson
+// Bindings to idevice - https://github.com/jkcoxson/idevice
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define LOCKDOWN_PORT 62078
+
+typedef enum AfcFopenMode {
+ AfcRdOnly = 1,
+ AfcRw = 2,
+ AfcWrOnly = 3,
+ AfcWr = 4,
+ AfcAppend = 5,
+ AfcRdAppend = 6,
+} AfcFopenMode;
+
+/**
+ * Link type for creating hard or symbolic links
+ */
+typedef enum AfcLinkType {
+ Hard = 1,
+ Symbolic = 2,
+} AfcLinkType;
+
+typedef enum IdeviceErrorCode {
+ IdeviceSuccess = 0,
+ Socket = -1,
+ Tls = -2,
+ TlsBuilderFailed = -3,
+ Plist = -4,
+ Utf8 = -5,
+ UnexpectedResponse = -6,
+ GetProhibited = -7,
+ SessionInactive = -8,
+ InvalidHostID = -9,
+ NoEstablishedConnection = -10,
+ HeartbeatSleepyTime = -11,
+ HeartbeatTimeout = -12,
+ NotFound = -13,
+ CdtunnelPacketTooShort = -14,
+ CdtunnelPacketInvalidMagic = -15,
+ PacketSizeMismatch = -16,
+ Json = -17,
+ DeviceNotFound = -18,
+ DeviceLocked = -19,
+ UsbConnectionRefused = -20,
+ UsbBadCommand = -21,
+ UsbBadDevice = -22,
+ UsbBadVersion = -23,
+ BadBuildManifest = -24,
+ ImageNotMounted = -25,
+ Reqwest = -26,
+ InternalError = -27,
+ Xpc = -28,
+ NsKeyedArchiveError = -29,
+ UnknownAuxValueType = -30,
+ UnknownChannel = -31,
+ AddrParseError = -32,
+ DisableMemoryLimitFailed = -33,
+ NotEnoughBytes = -34,
+ Utf8Error = -35,
+ InvalidArgument = -36,
+ UnknownErrorType = -37,
+ PemParseFailed = -38,
+ MisagentFailure = -39,
+ InstallationProxyOperationFailed = -40,
+ Afc = -41,
+ UnknownAfcOpcode = -42,
+ InvalidAfcMagic = -43,
+ AfcMissingAttribute = -44,
+ AdapterIOFailed = -996,
+ ServiceNotFound = -997,
+ BufferTooSmall = -998,
+ InvalidString = -999,
+ InvalidArg = -1000,
+} IdeviceErrorCode;
+
+typedef enum IdeviceLogLevel {
+ Disabled = 0,
+ ErrorLevel = 1,
+ Warn = 2,
+ Info = 3,
+ Debug = 4,
+ Trace = 5,
+} IdeviceLogLevel;
+
+typedef enum IdeviceLoggerError {
+ Success = 0,
+ FileError = -1,
+ AlreadyInitialized = -2,
+ InvalidPathString = -3,
+} IdeviceLoggerError;
+
+typedef struct AdapterHandle AdapterHandle;
+
+typedef struct AfcClientHandle AfcClientHandle;
+
+/**
+ * Handle for an open file on the device
+ */
+typedef struct AfcFileHandle AfcFileHandle;
+
+typedef struct AmfiClientHandle AmfiClientHandle;
+
+typedef struct CoreDeviceProxyHandle CoreDeviceProxyHandle;
+
+/**
+ * Opaque handle to a DebugProxyClient
+ */
+typedef struct DebugProxyAdapterHandle DebugProxyAdapterHandle;
+
+typedef struct HeartbeatClientHandle HeartbeatClientHandle;
+
+/**
+ * Opaque C-compatible handle to an Idevice connection
+ */
+typedef struct IdeviceHandle IdeviceHandle;
+
+/**
+ * Opaque C-compatible handle to a PairingFile
+ */
+typedef struct IdevicePairingFile IdevicePairingFile;
+
+typedef struct IdeviceSocketHandle IdeviceSocketHandle;
+
+typedef struct ImageMounterHandle ImageMounterHandle;
+
+typedef struct InstallationProxyClientHandle InstallationProxyClientHandle;
+
+/**
+ * Opaque handle to a ProcessControlClient
+ */
+typedef struct LocationSimulationAdapterHandle LocationSimulationAdapterHandle;
+
+typedef struct LockdowndClientHandle LockdowndClientHandle;
+
+typedef struct MisagentClientHandle MisagentClientHandle;
+
+/**
+ * Opaque handle to a ProcessControlClient
+ */
+typedef struct ProcessControlAdapterHandle ProcessControlAdapterHandle;
+
+/**
+ * Opaque handle to a RemoteServerClient
+ */
+typedef struct RemoteServerAdapterHandle RemoteServerAdapterHandle;
+
+typedef struct SpringBoardServicesClientHandle SpringBoardServicesClientHandle;
+
+typedef struct TcpProviderHandle TcpProviderHandle;
+
+typedef struct UsbmuxdAddrHandle UsbmuxdAddrHandle;
+
+typedef struct UsbmuxdConnectionHandle UsbmuxdConnectionHandle;
+
+typedef struct UsbmuxdProviderHandle UsbmuxdProviderHandle;
+
+/**
+ * Opaque handle to an XPCDevice
+ */
+typedef struct XPCDeviceAdapterHandle XPCDeviceAdapterHandle;
+
+typedef struct sockaddr sockaddr;
+
+/**
+ * File information structure for C bindings
+ */
+typedef struct AfcFileInfo {
+ size_t size;
+ size_t blocks;
+ int64_t creation;
+ int64_t modified;
+ char *st_nlink;
+ char *st_ifmt;
+ char *st_link_target;
+} AfcFileInfo;
+
+/**
+ * Device information structure for C bindings
+ */
+typedef struct AfcDeviceInfo {
+ char *model;
+ size_t total_bytes;
+ size_t free_bytes;
+ size_t block_size;
+} AfcDeviceInfo;
+
+/**
+ * Represents a debugserver command
+ */
+typedef struct DebugserverCommandHandle {
+ char *name;
+ char **argv;
+ uintptr_t argv_count;
+} DebugserverCommandHandle;
+
+/**
+ * Opaque handle to an XPCService
+ */
+typedef struct XPCServiceHandle {
+ char *entitlement;
+ uint16_t port;
+ bool uses_remote_xpc;
+ char **features;
+ uintptr_t features_count;
+ int64_t service_version;
+} XPCServiceHandle;
+
+/**
+ * Creates a new Idevice connection
+ *
+ * # Arguments
+ * * [`socket`] - Socket for communication with the device
+ * * [`label`] - Label for the connection
+ * * [`idevice`] - On success, will be set to point to a newly allocated Idevice handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `label` must be a valid null-terminated C string
+ * `idevice` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode idevice_new(struct IdeviceSocketHandle *socket,
+ const char *label,
+ struct IdeviceHandle **idevice);
+
+/**
+ * Creates a new Idevice connection
+ *
+ * # Arguments
+ * * [`addr`] - The socket address to connect to
+ * * [`addr_len`] - Length of the socket
+ * * [`label`] - Label for the connection
+ * * [`idevice`] - On success, will be set to point to a newly allocated Idevice handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `addr` must be a valid sockaddr
+ * `label` must be a valid null-terminated C string
+ * `idevice` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode idevice_new_tcp_socket(const struct sockaddr *addr,
+ socklen_t addr_len,
+ const char *label,
+ struct IdeviceHandle **idevice);
+
+/**
+ * Gets the device type
+ *
+ * # Arguments
+ * * [`idevice`] - The Idevice handle
+ * * [`device_type`] - On success, will be set to point to a newly allocated string containing the device type
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `idevice` must be a valid, non-null pointer to an Idevice handle
+ * `device_type` must be a valid, non-null pointer to a location where the string pointer will be stored
+ */
+enum IdeviceErrorCode idevice_get_type(struct IdeviceHandle *idevice,
+ char **device_type);
+
+/**
+ * Performs RSD checkin
+ *
+ * # Arguments
+ * * [`idevice`] - The Idevice handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `idevice` must be a valid, non-null pointer to an Idevice handle
+ */
+enum IdeviceErrorCode idevice_rsd_checkin(struct IdeviceHandle *idevice);
+
+/**
+ * Starts a TLS session
+ *
+ * # Arguments
+ * * [`idevice`] - The Idevice handle
+ * * [`pairing_file`] - The pairing file to use for TLS
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `idevice` must be a valid, non-null pointer to an Idevice handle
+ * `pairing_file` must be a valid, non-null pointer to a pairing file handle
+ */
+enum IdeviceErrorCode idevice_start_session(struct IdeviceHandle *idevice,
+ const struct IdevicePairingFile *pairing_file);
+
+/**
+ * Frees an Idevice handle
+ *
+ * # Arguments
+ * * [`idevice`] - The Idevice handle to free
+ *
+ * # Safety
+ * `idevice` must be a valid pointer to an Idevice handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void idevice_free(struct IdeviceHandle *idevice);
+
+/**
+ * Frees a string allocated by this library
+ *
+ * # Arguments
+ * * [`string`] - The string to free
+ *
+ * # Safety
+ * `string` must be a valid pointer to a string that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void idevice_string_free(char *string);
+
+/**
+ * Connects the adapter to a specific port
+ *
+ * # Arguments
+ * * [`handle`] - The adapter handle
+ * * [`port`] - The port to connect to
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode adapter_connect(struct AdapterHandle *handle, uint16_t port);
+
+/**
+ * Enables PCAP logging for the adapter
+ *
+ * # Arguments
+ * * [`handle`] - The adapter handle
+ * * [`path`] - The path to save the PCAP file (null-terminated string)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `path` must be a valid null-terminated string
+ */
+enum IdeviceErrorCode adapter_pcap(struct AdapterHandle *handle, const char *path);
+
+/**
+ * Closes the adapter connection
+ *
+ * # Arguments
+ * * [`handle`] - The adapter handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode adapter_close(struct AdapterHandle *handle);
+
+/**
+ * Sends data through the adapter
+ *
+ * # Arguments
+ * * [`handle`] - The adapter handle
+ * * [`data`] - The data to send
+ * * [`length`] - The length of the data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `data` must be a valid pointer to at least `length` bytes
+ */
+enum IdeviceErrorCode adapter_send(struct AdapterHandle *handle,
+ const uint8_t *data,
+ uintptr_t length);
+
+/**
+ * Receives data from the adapter
+ *
+ * # Arguments
+ * * [`handle`] - The adapter handle
+ * * [`data`] - Pointer to a buffer where the received data will be stored
+ * * [`length`] - Pointer to store the actual length of received data
+ * * [`max_length`] - Maximum number of bytes that can be stored in `data`
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `data` must be a valid pointer to at least `max_length` bytes
+ * `length` must be a valid pointer to a usize
+ */
+enum IdeviceErrorCode adapter_recv(struct AdapterHandle *handle,
+ uint8_t *data,
+ uintptr_t *length,
+ uintptr_t max_length);
+
+/**
+ * Connects to the AFC service using a TCP provider
+ *
+ * # Arguments
+ * * [`provider`] - A TcpProvider
+ * * [`client`] - On success, will be set to point to a newly allocated AfcClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode afc_client_connect_tcp(struct TcpProviderHandle *provider,
+ struct AfcClientHandle **client);
+
+/**
+ * Connects to the AFC service using a Usbmuxd provider
+ *
+ * # Arguments
+ * * [`provider`] - A UsbmuxdProvider
+ * * [`client`] - On success, will be set to point to a newly allocated AfcClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode afc_client_connect_usbmuxd(struct UsbmuxdProviderHandle *provider,
+ struct AfcClientHandle **client);
+
+/**
+ * Creates a new AfcClient from an existing Idevice connection
+ *
+ * # Arguments
+ * * [`socket`] - An IdeviceSocket handle
+ * * [`client`] - On success, will be set to point to a newly allocated AfcClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode afc_client_new(struct IdeviceHandle *socket, struct AfcClientHandle **client);
+
+/**
+ * Frees an AfcClient handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to the handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void afc_client_free(struct AfcClientHandle *handle);
+
+/**
+ * Lists the contents of a directory on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid AfcClient handle
+ * * [`path`] - Path to the directory to list (UTF-8 null-terminated)
+ * * [`entries`] - Will be set to point to an array of directory entries
+ * * [`count`] - Will be set to the number of entries
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ * `path` must be a valid null-terminated C string
+ */
+enum IdeviceErrorCode afc_list_directory(struct AfcClientHandle *client,
+ const char *path,
+ char ***entries,
+ size_t *count);
+
+/**
+ * Creates a new directory on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid AfcClient handle
+ * * [`path`] - Path of the directory to create (UTF-8 null-terminated)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `path` must be a valid null-terminated C string
+ */
+enum IdeviceErrorCode afc_make_directory(struct AfcClientHandle *client, const char *path);
+
+/**
+ * Retrieves information about a file or directory
+ *
+ * # Arguments
+ * * [`client`] - A valid AfcClient handle
+ * * [`path`] - Path to the file or directory (UTF-8 null-terminated)
+ * * [`info`] - Will be populated with file information
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` and `path` must be valid pointers
+ * `info` must be a valid pointer to an AfcFileInfo struct
+ */
+enum IdeviceErrorCode afc_get_file_info(struct AfcClientHandle *client,
+ const char *path,
+ struct AfcFileInfo *info);
+
+/**
+ * Frees memory allocated by afc_get_file_info
+ *
+ * # Arguments
+ * * [`info`] - Pointer to AfcFileInfo struct to free
+ *
+ * # Safety
+ * `info` must be a valid pointer to an AfcFileInfo struct previously returned by afc_get_file_info
+ */
+void afc_file_info_free(struct AfcFileInfo *info);
+
+/**
+ * Retrieves information about the device's filesystem
+ *
+ * # Arguments
+ * * [`client`] - A valid AfcClient handle
+ * * [`info`] - Will be populated with device information
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` and `info` must be valid pointers
+ */
+enum IdeviceErrorCode afc_get_device_info(struct AfcClientHandle *client,
+ struct AfcDeviceInfo *info);
+
+/**
+ * Frees memory allocated by afc_get_device_info
+ *
+ * # Arguments
+ * * [`info`] - Pointer to AfcDeviceInfo struct to free
+ *
+ * # Safety
+ * `info` must be a valid pointer to an AfcDeviceInfo struct previously returned by afc_get_device_info
+ */
+void afc_device_info_free(struct AfcDeviceInfo *info);
+
+/**
+ * Removes a file or directory
+ *
+ * # Arguments
+ * * [`client`] - A valid AfcClient handle
+ * * [`path`] - Path to the file or directory to remove (UTF-8 null-terminated)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `path` must be a valid null-terminated C string
+ */
+enum IdeviceErrorCode afc_remove_path(struct AfcClientHandle *client, const char *path);
+
+/**
+ * Recursively removes a directory and all its contents
+ *
+ * # Arguments
+ * * [`client`] - A valid AfcClient handle
+ * * [`path`] - Path to the directory to remove (UTF-8 null-terminated)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `path` must be a valid null-terminated C string
+ */
+enum IdeviceErrorCode afc_remove_path_and_contents(struct AfcClientHandle *client,
+ const char *path);
+
+/**
+ * Opens a file on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid AfcClient handle
+ * * [`path`] - Path to the file to open (UTF-8 null-terminated)
+ * * [`mode`] - File open mode
+ * * [`handle`] - Will be set to a new file handle on success
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ * `path` must be a valid null-terminated C string
+ */
+enum IdeviceErrorCode afc_file_open(struct AfcClientHandle *client,
+ const char *path,
+ enum AfcFopenMode mode,
+ struct AfcFileHandle **handle);
+
+/**
+ * Closes a file handle
+ *
+ * # Arguments
+ * * [`handle`] - File handle to close
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode afc_file_close(struct AfcFileHandle *handle);
+
+/**
+ * Reads data from an open file
+ *
+ * # Arguments
+ * * [`handle`] - File handle to read from
+ * * [`data`] - Will be set to point to the read data
+ * * [`length`] - Will be set to the length of the read data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ */
+enum IdeviceErrorCode afc_file_read(struct AfcFileHandle *handle, uint8_t **data, size_t *length);
+
+/**
+ * Writes data to an open file
+ *
+ * # Arguments
+ * * [`handle`] - File handle to write to
+ * * [`data`] - Data to write
+ * * [`length`] - Length of data to write
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ * `data` must point to at least `length` bytes
+ */
+enum IdeviceErrorCode afc_file_write(struct AfcFileHandle *handle,
+ const uint8_t *data,
+ size_t length);
+
+/**
+ * Creates a hard or symbolic link
+ *
+ * # Arguments
+ * * [`client`] - A valid AfcClient handle
+ * * [`target`] - Target path of the link (UTF-8 null-terminated)
+ * * [`source`] - Path where the link should be created (UTF-8 null-terminated)
+ * * [`link_type`] - Type of link to create
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ * `target` and `source` must be valid null-terminated C strings
+ */
+enum IdeviceErrorCode afc_make_link(struct AfcClientHandle *client,
+ const char *target,
+ const char *source,
+ enum AfcLinkType link_type);
+
+/**
+ * Renames a file or directory
+ *
+ * # Arguments
+ * * [`client`] - A valid AfcClient handle
+ * * [`source`] - Current path of the file/directory (UTF-8 null-terminated)
+ * * [`target`] - New path for the file/directory (UTF-8 null-terminated)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ * `source` and `target` must be valid null-terminated C strings
+ */
+enum IdeviceErrorCode afc_rename_path(struct AfcClientHandle *client,
+ const char *source,
+ const char *target);
+
+/**
+ * Automatically creates and connects to AMFI service, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A TcpProvider
+ * * [`client`] - On success, will be set to point to a newly allocated AmfiClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode amfi_connect_tcp(struct TcpProviderHandle *provider,
+ struct AmfiClientHandle **client);
+
+/**
+ * Automatically creates and connects to AMFI service, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A UsbmuxdProvider
+ * * [`client`] - On success, will be set to point to a newly allocated AmfiClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode amfi_connect_usbmuxd(struct UsbmuxdProviderHandle *provider,
+ struct AmfiClientHandle **client);
+
+/**
+ * Automatically creates and connects to AMFI service, returning a client handle
+ *
+ * # Arguments
+ * * [`socket`] - An IdeviceSocket handle
+ * * [`client`] - On success, will be set to point to a newly allocated AmfiClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode amfi_new(struct IdeviceHandle *socket, struct AmfiClientHandle **client);
+
+/**
+ * Shows the option in the settings UI
+ *
+ * # Arguments
+ * * `client` - A valid AmfiClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode amfi_reveal_developer_mode_option_in_ui(struct AmfiClientHandle *client);
+
+/**
+ * Enables developer mode on the device
+ *
+ * # Arguments
+ * * `client` - A valid AmfiClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode amfi_enable_developer_mode(struct AmfiClientHandle *client);
+
+/**
+ * Accepts developer mode on the device
+ *
+ * # Arguments
+ * * `client` - A valid AmfiClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode amfi_accept_developer_mode(struct AmfiClientHandle *client);
+
+/**
+ * Frees a handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to the handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void amfi_client_free(struct AmfiClientHandle *handle);
+
+/**
+ * Automatically creates and connects to Core Device Proxy, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A TcpProvider
+ * * [`client`] - On success, will be set to point to a newly allocated CoreDeviceProxy handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode core_device_proxy_connect_tcp(struct TcpProviderHandle *provider,
+ struct CoreDeviceProxyHandle **client);
+
+/**
+ * Automatically creates and connects to Core Device Proxy, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A UsbmuxdProvider
+ * * [`client`] - On success, will be set to point to a newly allocated CoreDeviceProxy handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode core_device_proxy_connect_usbmuxd(struct UsbmuxdProviderHandle *provider,
+ struct CoreDeviceProxyHandle **client);
+
+/**
+ * Automatically creates and connects to Core Device Proxy, returning a client handle
+ *
+ * # Arguments
+ * * [`socket`] - An IdeviceSocket handle
+ * * [`client`] - On success, will be set to point to a newly allocated CoreDeviceProxy handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode core_device_proxy_new(struct IdeviceHandle *socket,
+ struct CoreDeviceProxyHandle **client);
+
+/**
+ * Sends data through the CoreDeviceProxy tunnel
+ *
+ * # Arguments
+ * * [`handle`] - The CoreDeviceProxy handle
+ * * [`data`] - The data to send
+ * * [`length`] - The length of the data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `data` must be a valid pointer to at least `length` bytes
+ */
+enum IdeviceErrorCode core_device_proxy_send(struct CoreDeviceProxyHandle *handle,
+ const uint8_t *data,
+ uintptr_t length);
+
+/**
+ * Receives data from the CoreDeviceProxy tunnel
+ *
+ * # Arguments
+ * * [`handle`] - The CoreDeviceProxy handle
+ * * [`data`] - Pointer to a buffer where the received data will be stored
+ * * [`length`] - Pointer to store the actual length of received data
+ * * [`max_length`] - Maximum number of bytes that can be stored in `data`
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `data` must be a valid pointer to at least `max_length` bytes
+ * `length` must be a valid pointer to a usize
+ */
+enum IdeviceErrorCode core_device_proxy_recv(struct CoreDeviceProxyHandle *handle,
+ uint8_t *data,
+ uintptr_t *length,
+ uintptr_t max_length);
+
+/**
+ * Gets the client parameters from the handshake
+ *
+ * # Arguments
+ * * [`handle`] - The CoreDeviceProxy handle
+ * * [`mtu`] - Pointer to store the MTU value
+ * * [`address`] - Pointer to store the IP address string (must be at least 16 bytes)
+ * * [`netmask`] - Pointer to store the netmask string (must be at least 16 bytes)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `mtu` must be a valid pointer to a u16
+ * `address` and `netmask` must be valid pointers to buffers of at least 16 bytes
+ */
+enum IdeviceErrorCode core_device_proxy_get_client_parameters(struct CoreDeviceProxyHandle *handle,
+ uint16_t *mtu,
+ char **address,
+ char **netmask);
+
+/**
+ * Gets the server address from the handshake
+ *
+ * # Arguments
+ * * [`handle`] - The CoreDeviceProxy handle
+ * * [`address`] - Pointer to store the server address string (must be at least 16 bytes)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `address` must be a valid pointer to a buffer of at least 16 bytes
+ */
+enum IdeviceErrorCode core_device_proxy_get_server_address(struct CoreDeviceProxyHandle *handle,
+ char **address);
+
+/**
+ * Gets the server RSD port from the handshake
+ *
+ * # Arguments
+ * * [`handle`] - The CoreDeviceProxy handle
+ * * [`port`] - Pointer to store the port number
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `port` must be a valid pointer to a u16
+ */
+enum IdeviceErrorCode core_device_proxy_get_server_rsd_port(struct CoreDeviceProxyHandle *handle,
+ uint16_t *port);
+
+/**
+ * Creates a software TCP tunnel adapter
+ *
+ * # Arguments
+ * * [`handle`] - The CoreDeviceProxy handle
+ * * [`adapter`] - Pointer to store the newly created adapter handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library, and never used again
+ * `adapter` must be a valid pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode core_device_proxy_create_tcp_adapter(struct CoreDeviceProxyHandle *handle,
+ struct AdapterHandle **adapter);
+
+/**
+ * Frees a handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to the handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void core_device_proxy_free(struct CoreDeviceProxyHandle *handle);
+
+/**
+ * Frees a handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to the handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void adapter_free(struct AdapterHandle *handle);
+
+/**
+ * Creates a new DebugserverCommand
+ *
+ * # Safety
+ * Caller must free with debugserver_command_free
+ */
+struct DebugserverCommandHandle *debugserver_command_new(const char *name,
+ const char *const *argv,
+ uintptr_t argv_count);
+
+/**
+ * Frees a DebugserverCommand
+ *
+ * # Safety
+ * `command` must be a valid pointer or NULL
+ */
+void debugserver_command_free(struct DebugserverCommandHandle *command);
+
+/**
+ * Creates a new DebugProxyClient
+ *
+ * # Arguments
+ * * [`socket`] - The socket to use for communication
+ * * [`handle`] - Pointer to store the newly created DebugProxyClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `handle` must be a valid pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode debug_proxy_adapter_new(struct AdapterHandle *socket,
+ struct DebugProxyAdapterHandle **handle);
+
+/**
+ * Frees a DebugProxyClient handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void debug_proxy_free(struct DebugProxyAdapterHandle *handle);
+
+/**
+ * Sends a command to the debug proxy
+ *
+ * # Arguments
+ * * [`handle`] - The DebugProxyClient handle
+ * * [`command`] - The command to send
+ * * [`response`] - Pointer to store the response (caller must free)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` and `command` must be valid pointers
+ * `response` must be a valid pointer to a location where the string will be stored
+ */
+enum IdeviceErrorCode debug_proxy_send_command(struct DebugProxyAdapterHandle *handle,
+ struct DebugserverCommandHandle *command,
+ char **response);
+
+/**
+ * Reads a response from the debug proxy
+ *
+ * # Arguments
+ * * [`handle`] - The DebugProxyClient handle
+ * * [`response`] - Pointer to store the response (caller must free)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer
+ * `response` must be a valid pointer to a location where the string will be stored
+ */
+enum IdeviceErrorCode debug_proxy_read_response(struct DebugProxyAdapterHandle *handle,
+ char **response);
+
+/**
+ * Sends raw data to the debug proxy
+ *
+ * # Arguments
+ * * [`handle`] - The DebugProxyClient handle
+ * * [`data`] - The data to send
+ * * [`len`] - Length of the data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer
+ * `data` must be a valid pointer to `len` bytes
+ */
+enum IdeviceErrorCode debug_proxy_send_raw(struct DebugProxyAdapterHandle *handle,
+ const uint8_t *data,
+ uintptr_t len);
+
+/**
+ * Reads data from the debug proxy
+ *
+ * # Arguments
+ * * [`handle`] - The DebugProxyClient handle
+ * * [`len`] - Maximum number of bytes to read
+ * * [`response`] - Pointer to store the response (caller must free)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer
+ * `response` must be a valid pointer to a location where the string will be stored
+ */
+enum IdeviceErrorCode debug_proxy_read(struct DebugProxyAdapterHandle *handle,
+ uintptr_t len,
+ char **response);
+
+/**
+ * Sets the argv for the debug proxy
+ *
+ * # Arguments
+ * * [`handle`] - The DebugProxyClient handle
+ * * [`argv`] - NULL-terminated array of arguments
+ * * [`argv_count`] - Number of arguments
+ * * [`response`] - Pointer to store the response (caller must free)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer
+ * `argv` must be a valid pointer to `argv_count` C strings or NULL
+ * `response` must be a valid pointer to a location where the string will be stored
+ */
+enum IdeviceErrorCode debug_proxy_set_argv(struct DebugProxyAdapterHandle *handle,
+ const char *const *argv,
+ uintptr_t argv_count,
+ char **response);
+
+/**
+ * Sends an ACK to the debug proxy
+ *
+ * # Arguments
+ * * [`handle`] - The DebugProxyClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer
+ */
+enum IdeviceErrorCode debug_proxy_send_ack(struct DebugProxyAdapterHandle *handle);
+
+/**
+ * Sends a NACK to the debug proxy
+ *
+ * # Arguments
+ * * [`handle`] - The DebugProxyClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer
+ */
+enum IdeviceErrorCode debug_proxy_send_nack(struct DebugProxyAdapterHandle *handle);
+
+/**
+ * Sets the ACK mode for the debug proxy
+ *
+ * # Arguments
+ * * [`handle`] - The DebugProxyClient handle
+ * * [`enabled`] - Whether ACK mode should be enabled
+ *
+ * # Safety
+ * `handle` must be a valid pointer
+ */
+void debug_proxy_set_ack_mode(struct DebugProxyAdapterHandle *handle, int enabled);
+
+/**
+ * Returns the underlying socket from a DebugProxyClient
+ *
+ * # Arguments
+ * * [`handle`] - The handle to get the socket from
+ * * [`adapter`] - The newly allocated ConnectionHandle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL, and never used again
+ */
+enum IdeviceErrorCode debug_proxy_adapter_into_inner(struct DebugProxyAdapterHandle *handle,
+ struct AdapterHandle **adapter);
+
+/**
+ * Automatically creates and connects to Installation Proxy, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A TcpProvider
+ * * [`client`] - On success, will be set to point to a newly allocated InstallationProxyClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode heartbeat_connect_tcp(struct TcpProviderHandle *provider,
+ struct HeartbeatClientHandle **client);
+
+/**
+ * Automatically creates and connects to Installation Proxy, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A UsbmuxdProvider
+ * * [`client`] - On success, will be set to point to a newly allocated InstallationProxyClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode heartbeat_connect_usbmuxd(struct UsbmuxdProviderHandle *provider,
+ struct HeartbeatClientHandle **client);
+
+/**
+ * Automatically creates and connects to Installation Proxy, returning a client handle
+ *
+ * # Arguments
+ * * [`socket`] - An IdeviceSocket handle
+ * * [`client`] - On success, will be set to point to a newly allocated InstallationProxyClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode heartbeat_new(struct IdeviceHandle *socket,
+ struct HeartbeatClientHandle **client);
+
+/**
+ * Sends a polo to the device
+ *
+ * # Arguments
+ * * `client` - A valid HeartbeatClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode heartbeat_send_polo(struct HeartbeatClientHandle *client);
+
+/**
+ * Sends a polo to the device
+ *
+ * # Arguments
+ * * `client` - A valid HeartbeatClient handle
+ * * `interval` - The time to wait for a marco
+ * * `new_interval` - A pointer to set the requested marco
+ *
+ * # Returns
+ * An error code indicating success or failure.
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode heartbeat_get_marco(struct HeartbeatClientHandle *client,
+ uint64_t interval,
+ uint64_t *new_interval);
+
+/**
+ * Frees a handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to the handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void heartbeat_client_free(struct HeartbeatClientHandle *handle);
+
+/**
+ * Automatically creates and connects to Installation Proxy, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A TcpProvider
+ * * [`client`] - On success, will be set to point to a newly allocated InstallationProxyClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode installation_proxy_connect_tcp(struct TcpProviderHandle *provider,
+ struct InstallationProxyClientHandle **client);
+
+/**
+ * Automatically creates and connects to Installation Proxy, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A UsbmuxdProvider
+ * * [`client`] - On success, will be set to point to a newly allocated InstallationProxyClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode installation_proxy_connect_usbmuxd(struct UsbmuxdProviderHandle *provider,
+ struct InstallationProxyClientHandle **client);
+
+/**
+ * Automatically creates and connects to Installation Proxy, returning a client handle
+ *
+ * # Arguments
+ * * [`socket`] - An IdeviceSocket handle
+ * * [`client`] - On success, will be set to point to a newly allocated InstallationProxyClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode installation_proxy_new(struct IdeviceHandle *socket,
+ struct InstallationProxyClientHandle **client);
+
+/**
+ * Gets installed apps on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid InstallationProxyClient handle
+ * * [`application_type`] - The application type to filter by (optional, NULL for "Any")
+ * * [`bundle_identifiers`] - The identifiers to filter by (optional, NULL for all apps)
+ * * [`out_result`] - On success, will be set to point to a newly allocated array of PlistRef
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `out_result` must be a valid, non-null pointer to a location where the result will be stored
+ */
+enum IdeviceErrorCode installation_proxy_get_apps(struct InstallationProxyClientHandle *client,
+ const char *application_type,
+ const char *const *bundle_identifiers,
+ size_t bundle_identifiers_len,
+ void **out_result,
+ size_t *out_result_len);
+
+/**
+ * Frees a handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to the handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void installation_proxy_client_free(struct InstallationProxyClientHandle *handle);
+
+/**
+ * Installs an application package on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid InstallationProxyClient handle
+ * * [`package_path`] - Path to the .ipa package in the AFC jail
+ * * [`options`] - Optional installation options as a plist dictionary (can be NULL)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `package_path` must be a valid C string
+ * `options` must be a valid plist dictionary or NULL
+ */
+enum IdeviceErrorCode installation_proxy_install(struct InstallationProxyClientHandle *client,
+ const char *package_path,
+ void *options);
+
+/**
+ * Installs an application package on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid InstallationProxyClient handle
+ * * [`package_path`] - Path to the .ipa package in the AFC jail
+ * * [`options`] - Optional installation options as a plist dictionary (can be NULL)
+ * * [`callback`] - Progress callback function
+ * * [`context`] - User context to pass to callback
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `package_path` must be a valid C string
+ * `options` must be a valid plist dictionary or NULL
+ */
+enum IdeviceErrorCode installation_proxy_install_with_callback(struct InstallationProxyClientHandle *client,
+ const char *package_path,
+ void *options,
+ void (*callback)(uint64_t progress,
+ void *context),
+ void *context);
+
+/**
+ * Upgrades an existing application on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid InstallationProxyClient handle
+ * * [`package_path`] - Path to the .ipa package in the AFC jail
+ * * [`options`] - Optional upgrade options as a plist dictionary (can be NULL)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `package_path` must be a valid C string
+ * `options` must be a valid plist dictionary or NULL
+ */
+enum IdeviceErrorCode installation_proxy_upgrade(struct InstallationProxyClientHandle *client,
+ const char *package_path,
+ void *options);
+
+/**
+ * Upgrades an existing application on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid InstallationProxyClient handle
+ * * [`package_path`] - Path to the .ipa package in the AFC jail
+ * * [`options`] - Optional upgrade options as a plist dictionary (can be NULL)
+ * * [`callback`] - Progress callback function
+ * * [`context`] - User context to pass to callback
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `package_path` must be a valid C string
+ * `options` must be a valid plist dictionary or NULL
+ */
+enum IdeviceErrorCode installation_proxy_upgrade_with_callback(struct InstallationProxyClientHandle *client,
+ const char *package_path,
+ void *options,
+ void (*callback)(uint64_t progress,
+ void *context),
+ void *context);
+
+/**
+ * Uninstalls an application from the device
+ *
+ * # Arguments
+ * * [`client`] - A valid InstallationProxyClient handle
+ * * [`bundle_id`] - Bundle identifier of the application to uninstall
+ * * [`options`] - Optional uninstall options as a plist dictionary (can be NULL)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `bundle_id` must be a valid C string
+ * `options` must be a valid plist dictionary or NULL
+ */
+enum IdeviceErrorCode installation_proxy_uninstall(struct InstallationProxyClientHandle *client,
+ const char *bundle_id,
+ void *options);
+
+/**
+ * Uninstalls an application from the device
+ *
+ * # Arguments
+ * * [`client`] - A valid InstallationProxyClient handle
+ * * [`bundle_id`] - Bundle identifier of the application to uninstall
+ * * [`options`] - Optional uninstall options as a plist dictionary (can be NULL)
+ * * [`callback`] - Progress callback function
+ * * [`context`] - User context to pass to callback
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `bundle_id` must be a valid C string
+ * `options` must be a valid plist dictionary or NULL
+ */
+enum IdeviceErrorCode installation_proxy_uninstall_with_callback(struct InstallationProxyClientHandle *client,
+ const char *bundle_id,
+ void *options,
+ void (*callback)(uint64_t progress,
+ void *context),
+ void *context);
+
+/**
+ * Checks if the device capabilities match the required capabilities
+ *
+ * # Arguments
+ * * [`client`] - A valid InstallationProxyClient handle
+ * * [`capabilities`] - Array of plist values representing required capabilities
+ * * [`capabilities_len`] - Length of the capabilities array
+ * * [`options`] - Optional check options as a plist dictionary (can be NULL)
+ * * [`out_result`] - Will be set to true if all capabilities are supported, false otherwise
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `capabilities` must be a valid array of plist values or NULL
+ * `options` must be a valid plist dictionary or NULL
+ * `out_result` must be a valid pointer to a bool
+ */
+enum IdeviceErrorCode installation_proxy_check_capabilities_match(struct InstallationProxyClientHandle *client,
+ void *const *capabilities,
+ size_t capabilities_len,
+ void *options,
+ bool *out_result);
+
+/**
+ * Browses installed applications on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid InstallationProxyClient handle
+ * * [`options`] - Optional browse options as a plist dictionary (can be NULL)
+ * * [`out_result`] - On success, will be set to point to a newly allocated array of PlistRef
+ * * [`out_result_len`] - Will be set to the length of the result array
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `options` must be a valid plist dictionary or NULL
+ * `out_result` must be a valid, non-null pointer to a location where the result will be stored
+ * `out_result_len` must be a valid, non-null pointer to a location where the length will be stored
+ */
+enum IdeviceErrorCode installation_proxy_browse(struct InstallationProxyClientHandle *client,
+ void *options,
+ void **out_result,
+ size_t *out_result_len);
+
+/**
+ * Creates a new ProcessControlClient from a RemoteServerClient
+ *
+ * # Arguments
+ * * [`server`] - The RemoteServerClient to use
+ * * [`handle`] - Pointer to store the newly created ProcessControlClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `server` must be a valid pointer to a handle allocated by this library
+ * `handle` must be a valid pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode location_simulation_new(struct RemoteServerAdapterHandle *server,
+ struct LocationSimulationAdapterHandle **handle);
+
+/**
+ * Frees a ProcessControlClient handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void location_simulation_free(struct LocationSimulationAdapterHandle *handle);
+
+/**
+ * Clears the location set
+ *
+ * # Arguments
+ * * [`handle`] - The LocationSimulation handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid or NULL where appropriate
+ */
+enum IdeviceErrorCode location_simulation_clear(struct LocationSimulationAdapterHandle *handle);
+
+/**
+ * Sets the location
+ *
+ * # Arguments
+ * * [`handle`] - The LocationSimulation handle
+ * * [`latitude`] - The latitude to set
+ * * [`longitude`] - The longitude to set
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid or NULL where appropriate
+ */
+enum IdeviceErrorCode location_simulation_set(struct LocationSimulationAdapterHandle *handle,
+ double latitude,
+ double longitude);
+
+/**
+ * Connects to lockdownd service using TCP provider
+ *
+ * # Arguments
+ * * [`provider`] - A TcpProvider
+ * * [`client`] - On success, will be set to point to a newly allocated LockdowndClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode lockdownd_connect_tcp(struct TcpProviderHandle *provider,
+ struct LockdowndClientHandle **client);
+
+/**
+ * Connects to lockdownd service using Usbmuxd provider
+ *
+ * # Arguments
+ * * [`provider`] - A UsbmuxdProvider
+ * * [`client`] - On success, will be set to point to a newly allocated LockdowndClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode lockdownd_connect_usbmuxd(struct UsbmuxdProviderHandle *provider,
+ struct LockdowndClientHandle **client);
+
+/**
+ * Creates a new LockdowndClient from an existing Idevice connection
+ *
+ * # Arguments
+ * * [`socket`] - An IdeviceSocket handle
+ * * [`client`] - On success, will be set to point to a newly allocated LockdowndClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode lockdownd_new(struct IdeviceHandle *socket,
+ struct LockdowndClientHandle **client);
+
+/**
+ * Starts a session with lockdownd
+ *
+ * # Arguments
+ * * `client` - A valid LockdowndClient handle
+ * * `pairing_file` - An IdevicePairingFile alocated by this library
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `pairing_file` must be a valid plist_t containing a pairing file
+ */
+enum IdeviceErrorCode lockdownd_start_session(struct LockdowndClientHandle *client,
+ struct IdevicePairingFile *pairing_file);
+
+/**
+ * Starts a service through lockdownd
+ *
+ * # Arguments
+ * * `client` - A valid LockdowndClient handle
+ * * `identifier` - The service identifier to start (null-terminated string)
+ * * `port` - Pointer to store the returned port number
+ * * `ssl` - Pointer to store whether SSL should be enabled
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `identifier` must be a valid null-terminated string
+ * `port` and `ssl` must be valid pointers
+ */
+enum IdeviceErrorCode lockdownd_start_service(struct LockdowndClientHandle *client,
+ const char *identifier,
+ uint16_t *port,
+ bool *ssl);
+
+/**
+ * Gets a value from lockdownd
+ *
+ * # Arguments
+ * * `client` - A valid LockdowndClient handle
+ * * `key` - The value to get (null-terminated string)
+ * * `domain` - The value to get (null-terminated string)
+ * * `out_plist` - Pointer to store the returned plist value
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `value` must be a valid null-terminated string
+ * `out_plist` must be a valid pointer to store the plist
+ */
+enum IdeviceErrorCode lockdownd_get_value(struct LockdowndClientHandle *client,
+ const char *key,
+ const char *domain,
+ void **out_plist);
+
+/**
+ * Gets all values from lockdownd
+ *
+ * # Arguments
+ * * `client` - A valid LockdowndClient handle
+ * * `out_plist` - Pointer to store the returned plist dictionary
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `out_plist` must be a valid pointer to store the plist
+ */
+enum IdeviceErrorCode lockdownd_get_all_values(struct LockdowndClientHandle *client,
+ void **out_plist);
+
+/**
+ * Frees a LockdowndClient handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to the handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void lockdownd_client_free(struct LockdowndClientHandle *handle);
+
+/**
+ * Initializes the logger
+ *
+ * # Arguments
+ * * [`console_level`] - The level to log to the file
+ * * [`file_level`] - The level to log to the file
+ * * [`file_path`] - If not null, the file to write logs to
+ *
+ * ## Log Level
+ * 0. Disabled
+ * 1. Error
+ * 2. Warn
+ * 3. Info
+ * 4. Debug
+ * 5. Trace
+ *
+ * # Returns
+ * 0 for success, -1 if the file couldn't be created, -2 if a logger has been initialized, -3 for invalid path string
+ *
+ * # Safety
+ * Pass a valid CString for file_path. Pass valid log levels according to the enum
+ */
+enum IdeviceLoggerError idevice_init_logger(enum IdeviceLogLevel console_level,
+ enum IdeviceLogLevel file_level,
+ char *file_path);
+
+/**
+ * Automatically creates and connects to Misagent, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A TcpProvider
+ * * [`client`] - On success, will be set to point to a newly allocated MisagentClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode misagent_connect_tcp(struct TcpProviderHandle *provider,
+ struct MisagentClientHandle **client);
+
+/**
+ * Automatically creates and connects to Misagent, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A UsbmuxdProvider
+ * * [`client`] - On success, will be set to point to a newly allocated MisagentClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode misagent_connect_usbmuxd(struct UsbmuxdProviderHandle *provider,
+ struct MisagentClientHandle **client);
+
+/**
+ * Installs a provisioning profile on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid MisagentClient handle
+ * * [`profile_data`] - The provisioning profile data to install
+ * * [`profile_len`] - Length of the profile data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `profile_data` must be a valid pointer to profile data of length `profile_len`
+ */
+enum IdeviceErrorCode misagent_install(struct MisagentClientHandle *client,
+ const uint8_t *profile_data,
+ size_t profile_len);
+
+/**
+ * Removes a provisioning profile from the device
+ *
+ * # Arguments
+ * * [`client`] - A valid MisagentClient handle
+ * * [`profile_id`] - The UUID of the profile to remove (C string)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `profile_id` must be a valid C string
+ */
+enum IdeviceErrorCode misagent_remove(struct MisagentClientHandle *client, const char *profile_id);
+
+/**
+ * Retrieves all provisioning profiles from the device
+ *
+ * # Arguments
+ * * [`client`] - A valid MisagentClient handle
+ * * [`out_profiles`] - On success, will be set to point to an array of profile data
+ * * [`out_profiles_len`] - On success, will be set to the number of profiles
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `out_profiles` must be a valid pointer to store the resulting array
+ * `out_profiles_len` must be a valid pointer to store the array length
+ */
+enum IdeviceErrorCode misagent_copy_all(struct MisagentClientHandle *client,
+ uint8_t ***out_profiles,
+ size_t **out_profiles_len,
+ size_t *out_count);
+
+/**
+ * Frees profiles array returned by misagent_copy_all
+ *
+ * # Arguments
+ * * [`profiles`] - Array of profile data pointers
+ * * [`lens`] - Array of profile lengths
+ * * [`count`] - Number of profiles in the array
+ *
+ * # Safety
+ * Must only be called with values returned from misagent_copy_all
+ */
+void misagent_free_profiles(uint8_t **profiles, size_t *lens, size_t count);
+
+/**
+ * Frees a misagent client handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void misagent_client_free(struct MisagentClientHandle *handle);
+
+/**
+ * Connects to the Image Mounter service using a TCP provider
+ *
+ * # Arguments
+ * * [`provider`] - A TcpProvider
+ * * [`client`] - On success, will be set to point to a newly allocated ImageMounter handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode image_mounter_connect_tcp(struct TcpProviderHandle *provider,
+ struct ImageMounterHandle **client);
+
+/**
+ * Connects to the Image Mounter service using a Usbmuxd provider
+ *
+ * # Arguments
+ * * [`provider`] - A UsbmuxdProvider
+ * * [`client`] - On success, will be set to point to a newly allocated ImageMounter handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode image_mounter_connect_usbmuxd(struct UsbmuxdProviderHandle *provider,
+ struct ImageMounterHandle **client);
+
+/**
+ * Creates a new ImageMounter client from an existing Idevice connection
+ *
+ * # Arguments
+ * * [`socket`] - An IdeviceSocket handle
+ * * [`client`] - On success, will be set to point to a newly allocated ImageMounter handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode image_mounter_new(struct IdeviceHandle *socket,
+ struct ImageMounterHandle **client);
+
+/**
+ * Frees an ImageMounter handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to the handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void image_mounter_free(struct ImageMounterHandle *handle);
+
+/**
+ * Gets a list of mounted devices
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`devices`] - Will be set to point to a slice of device plists on success
+ * * [`devices_len`] - Will be set to the number of devices copied
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `devices` must be a valid, non-null pointer to a location where the plist will be stored
+ */
+enum IdeviceErrorCode image_mounter_copy_devices(struct ImageMounterHandle *client,
+ void **devices,
+ size_t *devices_len);
+
+/**
+ * Looks up an image and returns its signature
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`image_type`] - The type of image to look up
+ * * [`signature`] - Will be set to point to the signature data on success
+ * * [`signature_len`] - Will be set to the length of the signature data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `image_type` must be a valid null-terminated C string
+ * `signature` and `signature_len` must be valid pointers
+ */
+enum IdeviceErrorCode image_mounter_lookup_image(struct ImageMounterHandle *client,
+ const char *image_type,
+ uint8_t **signature,
+ size_t *signature_len);
+
+/**
+ * Uploads an image to the device
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`image_type`] - The type of image being uploaded
+ * * [`image`] - Pointer to the image data
+ * * [`image_len`] - Length of the image data
+ * * [`signature`] - Pointer to the signature data
+ * * [`signature_len`] - Length of the signature data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ * `image_type` must be a valid null-terminated C string
+ */
+enum IdeviceErrorCode image_mounter_upload_image(struct ImageMounterHandle *client,
+ const char *image_type,
+ const uint8_t *image,
+ size_t image_len,
+ const uint8_t *signature,
+ size_t signature_len);
+
+/**
+ * Mounts an image on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`image_type`] - The type of image being mounted
+ * * [`signature`] - Pointer to the signature data
+ * * [`signature_len`] - Length of the signature data
+ * * [`trust_cache`] - Pointer to trust cache data (optional)
+ * * [`trust_cache_len`] - Length of trust cache data (0 if none)
+ * * [`info_plist`] - Pointer to info plist (optional)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid (except optional ones which can be null)
+ * `image_type` must be a valid null-terminated C string
+ */
+enum IdeviceErrorCode image_mounter_mount_image(struct ImageMounterHandle *client,
+ const char *image_type,
+ const uint8_t *signature,
+ size_t signature_len,
+ const uint8_t *trust_cache,
+ size_t trust_cache_len,
+ const void *info_plist);
+
+/**
+ * Unmounts an image from the device
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`mount_path`] - The path where the image is mounted
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `mount_path` must be a valid null-terminated C string
+ */
+enum IdeviceErrorCode image_mounter_unmount_image(struct ImageMounterHandle *client,
+ const char *mount_path);
+
+/**
+ * Queries the developer mode status
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`status`] - Will be set to the developer mode status (1 = enabled, 0 = disabled)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `status` must be a valid pointer
+ */
+enum IdeviceErrorCode image_mounter_query_developer_mode_status(struct ImageMounterHandle *client,
+ int *status);
+
+/**
+ * Mounts a developer image
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`image`] - Pointer to the image data
+ * * [`image_len`] - Length of the image data
+ * * [`signature`] - Pointer to the signature data
+ * * [`signature_len`] - Length of the signature data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ */
+enum IdeviceErrorCode image_mounter_mount_developer(struct ImageMounterHandle *client,
+ const uint8_t *image,
+ size_t image_len,
+ const uint8_t *signature,
+ size_t signature_len);
+
+/**
+ * Queries the personalization manifest from the device
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`image_type`] - The type of image to query
+ * * [`signature`] - Pointer to the signature data
+ * * [`signature_len`] - Length of the signature data
+ * * [`manifest`] - Will be set to point to the manifest data on success
+ * * [`manifest_len`] - Will be set to the length of the manifest data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ * `image_type` must be a valid null-terminated C string
+ */
+enum IdeviceErrorCode image_mounter_query_personalization_manifest(struct ImageMounterHandle *client,
+ const char *image_type,
+ const uint8_t *signature,
+ size_t signature_len,
+ uint8_t **manifest,
+ size_t *manifest_len);
+
+/**
+ * Queries the nonce from the device
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`personalized_image_type`] - The type of image to query (optional)
+ * * [`nonce`] - Will be set to point to the nonce data on success
+ * * [`nonce_len`] - Will be set to the length of the nonce data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client`, `nonce`, and `nonce_len` must be valid pointers
+ * `personalized_image_type` can be NULL
+ */
+enum IdeviceErrorCode image_mounter_query_nonce(struct ImageMounterHandle *client,
+ const char *personalized_image_type,
+ uint8_t **nonce,
+ size_t *nonce_len);
+
+/**
+ * Queries personalization identifiers from the device
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`image_type`] - The type of image to query (optional)
+ * * [`identifiers`] - Will be set to point to the identifiers plist on success
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` and `identifiers` must be valid pointers
+ * `image_type` can be NULL
+ */
+enum IdeviceErrorCode image_mounter_query_personalization_identifiers(struct ImageMounterHandle *client,
+ const char *image_type,
+ void **identifiers);
+
+/**
+ * Rolls the personalization nonce
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode image_mounter_roll_personalization_nonce(struct ImageMounterHandle *client);
+
+/**
+ * Rolls the cryptex nonce
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode image_mounter_roll_cryptex_nonce(struct ImageMounterHandle *client);
+
+/**
+ * Mounts a personalized developer image
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`provider`] - A valid provider handle
+ * * [`image`] - Pointer to the image data
+ * * [`image_len`] - Length of the image data
+ * * [`trust_cache`] - Pointer to the trust cache data
+ * * [`trust_cache_len`] - Length of the trust cache data
+ * * [`build_manifest`] - Pointer to the build manifest data
+ * * [`build_manifest_len`] - Length of the build manifest data
+ * * [`info_plist`] - Pointer to info plist (optional)
+ * * [`unique_chip_id`] - The device's unique chip ID
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid (except optional ones which can be null)
+ */
+enum IdeviceErrorCode image_mounter_mount_personalized_usbmuxd(struct ImageMounterHandle *client,
+ struct UsbmuxdProviderHandle *provider,
+ const uint8_t *image,
+ size_t image_len,
+ const uint8_t *trust_cache,
+ size_t trust_cache_len,
+ const uint8_t *build_manifest,
+ size_t build_manifest_len,
+ const void *info_plist,
+ uint64_t unique_chip_id);
+
+/**
+ * Mounts a personalized developer image
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`provider`] - A valid provider handle
+ * * [`image`] - Pointer to the image data
+ * * [`image_len`] - Length of the image data
+ * * [`trust_cache`] - Pointer to the trust cache data
+ * * [`trust_cache_len`] - Length of the trust cache data
+ * * [`build_manifest`] - Pointer to the build manifest data
+ * * [`build_manifest_len`] - Length of the build manifest data
+ * * [`info_plist`] - Pointer to info plist (optional)
+ * * [`unique_chip_id`] - The device's unique chip ID
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid (except optional ones which can be null)
+ */
+enum IdeviceErrorCode image_mounter_mount_personalized_tcp(struct ImageMounterHandle *client,
+ struct TcpProviderHandle *provider,
+ const uint8_t *image,
+ size_t image_len,
+ const uint8_t *trust_cache,
+ size_t trust_cache_len,
+ const uint8_t *build_manifest,
+ size_t build_manifest_len,
+ const void *info_plist,
+ uint64_t unique_chip_id);
+
+/**
+ * Mounts a personalized developer image with progress callback
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`provider`] - A valid provider handle
+ * * [`image`] - Pointer to the image data
+ * * [`image_len`] - Length of the image data
+ * * [`trust_cache`] - Pointer to the trust cache data
+ * * [`trust_cache_len`] - Length of the trust cache data
+ * * [`build_manifest`] - Pointer to the build manifest data
+ * * [`build_manifest_len`] - Length of the build manifest data
+ * * [`info_plist`] - Pointer to info plist (optional)
+ * * [`unique_chip_id`] - The device's unique chip ID
+ * * [`callback`] - Progress callback function
+ * * [`context`] - User context to pass to callback
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid (except optional ones which can be null)
+ */
+enum IdeviceErrorCode image_mounter_mount_personalized_usbmuxd_with_callback(struct ImageMounterHandle *client,
+ struct UsbmuxdProviderHandle *provider,
+ const uint8_t *image,
+ size_t image_len,
+ const uint8_t *trust_cache,
+ size_t trust_cache_len,
+ const uint8_t *build_manifest,
+ size_t build_manifest_len,
+ const void *info_plist,
+ uint64_t unique_chip_id,
+ void (*callback)(size_t progress,
+ size_t total,
+ void *context),
+ void *context);
+
+/**
+ * Mounts a personalized developer image with progress callback
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`provider`] - A valid provider handle
+ * * [`image`] - Pointer to the image data
+ * * [`image_len`] - Length of the image data
+ * * [`trust_cache`] - Pointer to the trust cache data
+ * * [`trust_cache_len`] - Length of the trust cache data
+ * * [`build_manifest`] - Pointer to the build manifest data
+ * * [`build_manifest_len`] - Length of the build manifest data
+ * * [`info_plist`] - Pointer to info plist (optional)
+ * * [`unique_chip_id`] - The device's unique chip ID
+ * * [`callback`] - Progress callback function
+ * * [`context`] - User context to pass to callback
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid (except optional ones which can be null)
+ */
+enum IdeviceErrorCode image_mounter_mount_personalized_tcp_with_callback(struct ImageMounterHandle *client,
+ struct TcpProviderHandle *provider,
+ const uint8_t *image,
+ size_t image_len,
+ const uint8_t *trust_cache,
+ size_t trust_cache_len,
+ const uint8_t *build_manifest,
+ size_t build_manifest_len,
+ const void *info_plist,
+ uint64_t unique_chip_id,
+ void (*callback)(size_t progress,
+ size_t total,
+ void *context),
+ void *context);
+
+/**
+ * Reads a pairing file from the specified path
+ *
+ * # Arguments
+ * * [`path`] - Path to the pairing file
+ * * [`pairing_file`] - On success, will be set to point to a newly allocated pairing file instance
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `path` must be a valid null-terminated C string
+ * `pairing_file` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode idevice_pairing_file_read(const char *path,
+ struct IdevicePairingFile **pairing_file);
+
+/**
+ * Parses a pairing file from a byte buffer
+ *
+ * # Arguments
+ * * [`data`] - Pointer to the buffer containing pairing file data
+ * * [`size`] - Size of the buffer in bytes
+ * * [`pairing_file`] - On success, will be set to point to a newly allocated pairing file instance
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `data` must be a valid pointer to a buffer of at least `size` bytes
+ * `pairing_file` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode idevice_pairing_file_from_bytes(const uint8_t *data,
+ uintptr_t size,
+ struct IdevicePairingFile **pairing_file);
+
+/**
+ * Serializes a pairing file to XML format
+ *
+ * # Arguments
+ * * [`pairing_file`] - The pairing file to serialize
+ * * [`data`] - On success, will be set to point to a newly allocated buffer containing the serialized data
+ * * [`size`] - On success, will be set to the size of the allocated buffer
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `pairing_file` must be a valid, non-null pointer to a pairing file instance
+ * `data` must be a valid, non-null pointer to a location where the buffer pointer will be stored
+ * `size` must be a valid, non-null pointer to a location where the buffer size will be stored
+ */
+enum IdeviceErrorCode idevice_pairing_file_serialize(const struct IdevicePairingFile *pairing_file,
+ uint8_t **data,
+ uintptr_t *size);
+
+/**
+ * Frees a pairing file instance
+ *
+ * # Arguments
+ * * [`pairing_file`] - The pairing file to free
+ *
+ * # Safety
+ * `pairing_file` must be a valid pointer to a pairing file instance that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void idevice_pairing_file_free(struct IdevicePairingFile *pairing_file);
+
+/**
+ * Creates a new ProcessControlClient from a RemoteServerClient
+ *
+ * # Arguments
+ * * [`server`] - The RemoteServerClient to use
+ * * [`handle`] - Pointer to store the newly created ProcessControlClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `server` must be a valid pointer to a handle allocated by this library
+ * `handle` must be a valid pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode process_control_new(struct RemoteServerAdapterHandle *server,
+ struct ProcessControlAdapterHandle **handle);
+
+/**
+ * Frees a ProcessControlClient handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void process_control_free(struct ProcessControlAdapterHandle *handle);
+
+/**
+ * Launches an application on the device
+ *
+ * # Arguments
+ * * [`handle`] - The ProcessControlClient handle
+ * * [`bundle_id`] - The bundle identifier of the app to launch
+ * * [`env_vars`] - NULL-terminated array of environment variables (format "KEY=VALUE")
+ * * [`arguments`] - NULL-terminated array of arguments
+ * * [`start_suspended`] - Whether to start the app suspended
+ * * [`kill_existing`] - Whether to kill existing instances of the app
+ * * [`pid`] - Pointer to store the process ID of the launched app
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid or NULL where appropriate
+ */
+enum IdeviceErrorCode process_control_launch_app(struct ProcessControlAdapterHandle *handle,
+ const char *bundle_id,
+ const char *const *env_vars,
+ uintptr_t env_vars_count,
+ const char *const *arguments,
+ uintptr_t arguments_count,
+ bool start_suspended,
+ bool kill_existing,
+ uint64_t *pid);
+
+/**
+ * Kills a running process
+ *
+ * # Arguments
+ * * [`handle`] - The ProcessControlClient handle
+ * * [`pid`] - The process ID to kill
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode process_control_kill_app(struct ProcessControlAdapterHandle *handle,
+ uint64_t pid);
+
+/**
+ * Disables memory limits for a process
+ *
+ * # Arguments
+ * * [`handle`] - The ProcessControlClient handle
+ * * [`pid`] - The process ID to modify
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode process_control_disable_memory_limit(struct ProcessControlAdapterHandle *handle,
+ uint64_t pid);
+
+/**
+ * Creates a TCP provider for idevice
+ *
+ * # Arguments
+ * * [`ip`] - The sockaddr IP to connect to
+ * * [`pairing_file`] - The pairing file handle to use
+ * * [`label`] - The label to use with the connection
+ * * [`provider`] - A pointer to a newly allocated provider
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `ip` must be a valid sockaddr
+ * `pairing_file` must never be used again
+ * `label` must be a valid Cstr
+ * `provider` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode idevice_tcp_provider_new(const struct sockaddr *ip,
+ struct IdevicePairingFile *pairing_file,
+ const char *label,
+ struct TcpProviderHandle **provider);
+
+/**
+ * Frees a TcpProvider handle
+ *
+ * # Arguments
+ * * [`provider`] - The provider handle to free
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a TcpProvider handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void tcp_provider_free(struct TcpProviderHandle *provider);
+
+/**
+ * Creates a usbmuxd provider for idevice
+ *
+ * # Arguments
+ * * [`addr`] - The UsbmuxdAddr handle to connect to
+ * * [`tag`] - The tag returned in usbmuxd responses
+ * * [`udid`] - The UDID of the device to connect to
+ * * [`device_id`] - The muxer ID of the device to connect to
+ * * [`pairing_file`] - The pairing file handle to use
+ * * [`label`] - The label to use with the connection
+ * * [`provider`] - A pointer to a newly allocated provider
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `addr` must be a valid pointer to UsbmuxdAddrHandle created by this library, and never used again
+ * `udid` must be a valid CStr
+ * `pairing_file` must never be used again
+ * `label` must be a valid Cstr
+ * `provider` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode usbmuxd_provider_new(struct UsbmuxdAddrHandle *addr,
+ uint32_t tag,
+ const char *udid,
+ uint32_t device_id,
+ const char *label,
+ struct UsbmuxdProviderHandle **provider);
+
+/**
+ * Frees a UsbmuxdProvider handle
+ *
+ * # Arguments
+ * * [`provider`] - The provider handle to free
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a UsbmuxdProvider handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void usbmuxd_provider_free(struct UsbmuxdProviderHandle *provider);
+
+/**
+ * Creates a new RemoteServerClient from a ReadWrite connection
+ *
+ * # Arguments
+ * * [`connection`] - The connection to use for communication
+ * * [`handle`] - Pointer to store the newly created RemoteServerClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `connection` must be a valid pointer to a handle allocated by this library
+ * `handle` must be a valid pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode remote_server_adapter_new(struct AdapterHandle *adapter,
+ struct RemoteServerAdapterHandle **handle);
+
+/**
+ * Frees a RemoteServerClient handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void remote_server_free(struct RemoteServerAdapterHandle *handle);
+
+/**
+ * Returns the underlying connection from a RemoteServerClient
+ *
+ * # Arguments
+ * * [`handle`] - The handle to get the connection from
+ * * [`connection`] - The newly allocated ConnectionHandle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL, and never used again
+ */
+enum IdeviceErrorCode remote_server_adapter_into_inner(struct RemoteServerAdapterHandle *handle,
+ struct AdapterHandle **connection);
+
+/**
+ * Creates a new XPCDevice from an adapter
+ *
+ * # Arguments
+ * * [`adapter`] - The adapter to use for communication
+ * * [`device`] - Pointer to store the newly created XPCDevice handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `adapter` must be a valid pointer to a handle allocated by this library
+ * `device` must be a valid pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode xpc_device_new(struct AdapterHandle *adapter,
+ struct XPCDeviceAdapterHandle **device);
+
+/**
+ * Frees an XPCDevice handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void xpc_device_free(struct XPCDeviceAdapterHandle *handle);
+
+/**
+ * Gets a service by name from the XPCDevice
+ *
+ * # Arguments
+ * * [`handle`] - The XPCDevice handle
+ * * [`service_name`] - The name of the service to get
+ * * [`service`] - Pointer to store the newly created XPCService handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `service_name` must be a valid null-terminated C string
+ * `service` must be a valid pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode xpc_device_get_service(struct XPCDeviceAdapterHandle *handle,
+ const char *service_name,
+ struct XPCServiceHandle **service);
+
+/**
+ * Returns the adapter in the RemoteXPC Device
+ *
+ * # Arguments
+ * * [`handle`] - The handle to get the adapter from
+ * * [`adapter`] - The newly allocated AdapterHandle
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL, and never used again
+ */
+enum IdeviceErrorCode xpc_device_adapter_into_inner(struct XPCDeviceAdapterHandle *handle,
+ struct AdapterHandle **adapter);
+
+/**
+ * Frees an XPCService handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void xpc_service_free(struct XPCServiceHandle *handle);
+
+/**
+ * Gets the list of available service names
+ *
+ * # Arguments
+ * * [`handle`] - The XPCDevice handle
+ * * [`names`] - Pointer to store the array of service names
+ * * [`count`] - Pointer to store the number of services
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `names` must be a valid pointer to a location where the array will be stored
+ * `count` must be a valid pointer to a location where the count will be stored
+ */
+enum IdeviceErrorCode xpc_device_get_service_names(struct XPCDeviceAdapterHandle *handle,
+ char ***names,
+ uintptr_t *count);
+
+/**
+ * Frees a list of service names
+ *
+ * # Arguments
+ * * [`names`] - The array of service names to free
+ * * [`count`] - The number of services in the array
+ *
+ * # Safety
+ * `names` must be a valid pointer to an array of `count` C strings
+ */
+void xpc_device_free_service_names(char **names, uintptr_t count);
+
+/**
+ * Connects to the Springboard service using a TCP provider
+ *
+ * # Arguments
+ * * [`provider`] - A TcpProvider
+ * * [`client`] - On success, will be set to point to a newly allocated SpringBoardServicesClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode springboard_services_connect_tcp(struct TcpProviderHandle *provider,
+ struct SpringBoardServicesClientHandle **client);
+
+/**
+ * Connects to the Springboard service using a usbmuxd provider
+ *
+ * # Arguments
+ * * [`provider`] - A UsbmuxdProvider
+ * * [`client`] - On success, will be set to point to a newly allocated SpringBoardServicesClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode springboard_services_connect_usbmuxd(struct UsbmuxdProviderHandle *provider,
+ struct SpringBoardServicesClientHandle **client);
+
+/**
+ * Creates a new SpringBoardServices client from an existing Idevice connection
+ *
+ * # Arguments
+ * * [`socket`] - An IdeviceSocket handle
+ * * [`client`] - On success, will be set to point to a newly allocated SpringBoardServicesClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode springboard_services_new(struct IdeviceHandle *socket,
+ struct SpringBoardServicesClientHandle **client);
+
+/**
+ * Gets the icon of the specified app by bundle identifier
+ *
+ * # Arguments
+ * * `client` - A valid SpringBoardServicesClient handle
+ * * `bundle_identifier` - The identifiers of the app to get icon
+ * * `out_result` - On success, will be set to point to a newly allocated png data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `out_result` must be a valid, non-null pointer to a location where the result will be stored
+ */
+enum IdeviceErrorCode springboard_services_get_icon(struct SpringBoardServicesClientHandle *client,
+ const char *bundle_identifier,
+ void **out_result,
+ size_t *out_result_len);
+
+/**
+ * Frees an SpringBoardServicesClient handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to the handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void springboard_services_free(struct SpringBoardServicesClientHandle *handle);
+
+/**
+ * Connects to a usbmuxd instance over TCP
+ *
+ * # Arguments
+ * * [`addr`] - The socket address to connect to
+ * * [`addr_len`] - Length of the socket
+ * * [`tag`] - A tag that will be returned by usbmuxd responses
+ * * [`usbmuxd_connection`] - On success, will be set to point to a newly allocated UsbmuxdConnection handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `addr` must be a valid sockaddr
+ * `usbmuxd_connection` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode idevice_usbmuxd_new_tcp_connection(const struct sockaddr *addr,
+ socklen_t addr_len,
+ uint32_t tag,
+ struct UsbmuxdConnectionHandle **usbmuxd_connection);
+
+/**
+ * Connects to a usbmuxd instance over unix socket
+ *
+ * # Arguments
+ * * [`addr`] - The socket path to connect to
+ * * [`tag`] - A tag that will be returned by usbmuxd responses
+ * * [`usbmuxd_connection`] - On success, will be set to point to a newly allocated UsbmuxdConnection handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `addr` must be a valid CStr
+ * `usbmuxd_connection` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode idevice_usbmuxd_new_unix_socket_connection(const char *addr,
+ uint32_t tag,
+ struct UsbmuxdConnectionHandle **usbmuxd_connection);
+
+/**
+ * Frees a UsbmuxdConnection handle
+ *
+ * # Arguments
+ * * [`usbmuxd_connection`] - The UsbmuxdConnection handle to free
+ *
+ * # Safety
+ * `usbmuxd_connection` must be a valid pointer to a UsbmuxdConnection handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void idevice_usbmuxd_connection_free(struct UsbmuxdConnectionHandle *usbmuxd_connection);
+
+/**
+ * Creates a usbmuxd TCP address struct
+ *
+ * # Arguments
+ * * [`addr`] - The socket address to connect to
+ * * [`addr_len`] - Length of the socket
+ * * [`usbmuxd_addr`] - On success, will be set to point to a newly allocated UsbmuxdAddr handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `addr` must be a valid sockaddr
+ * `usbmuxd_Addr` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode idevice_usbmuxd_tcp_addr_new(const struct sockaddr *addr,
+ socklen_t addr_len,
+ struct UsbmuxdAddrHandle **usbmuxd_addr);
+
+/**
+ * Creates a new UsbmuxdAddr struct with a unix socket
+ *
+ * # Arguments
+ * * [`addr`] - The socket path to connect to
+ * * [`usbmuxd_addr`] - On success, will be set to point to a newly allocated UsbmuxdAddr handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `addr` must be a valid CStr
+ * `usbmuxd_addr` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode idevice_usbmuxd_unix_addr_new(const char *addr,
+ struct UsbmuxdAddrHandle **usbmuxd_addr);
+
+/**
+ * Frees a UsbmuxdAddr handle
+ *
+ * # Arguments
+ * * [`usbmuxd_addr`] - The UsbmuxdAddr handle to free
+ *
+ * # Safety
+ * `usbmuxd_addr` must be a valid pointer to a UsbmuxdAddr handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void idevice_usbmuxd_addr_free(struct UsbmuxdAddrHandle *usbmuxd_addr);
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/plist.h b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/plist.h
new file mode 100644
index 000000000..0e211278e
--- /dev/null
+++ b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/plist.h
@@ -0,0 +1,1096 @@
+/**
+ * @file plist/plist.h
+ * @brief Main include of libplist
+ * \internal
+ *
+ * Copyright (c) 2012-2023 Nikias Bassen, All Rights Reserved.
+ * Copyright (c) 2008-2009 Jonathan Beck, All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef LIBPLIST_H
+#define LIBPLIST_H
+
+#if _MSC_VER && _MSC_VER < 1700
+ typedef __int8 int8_t;
+ typedef __int16 int16_t;
+ typedef __int32 int32_t;
+ typedef __int64 int64_t;
+
+ typedef unsigned __int8 uint8_t;
+ typedef unsigned __int16 uint16_t;
+ typedef unsigned __int32 uint32_t;
+ typedef unsigned __int64 uint64_t;
+
+#else
+#include
+#endif
+
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+
+#ifdef __llvm__
+ #if defined(__has_extension)
+ #if (__has_extension(attribute_deprecated_with_message))
+ #ifndef PLIST_WARN_DEPRECATED
+ #define PLIST_WARN_DEPRECATED(x) __attribute__((deprecated(x)))
+ #endif
+ #else
+ #ifndef PLIST_WARN_DEPRECATED
+ #define PLIST_WARN_DEPRECATED(x) __attribute__((deprecated))
+ #endif
+ #endif
+ #else
+ #ifndef PLIST_WARN_DEPRECATED
+ #define PLIST_WARN_DEPRECATED(x) __attribute__((deprecated))
+ #endif
+ #endif
+#elif (__GNUC__ > 4 || (__GNUC__ == 4 && (__GNUC_MINOR__ >= 5)))
+ #ifndef PLIST_WARN_DEPRECATED
+ #define PLIST_WARN_DEPRECATED(x) __attribute__((deprecated(x)))
+ #endif
+#elif defined(_MSC_VER)
+ #ifndef PLIST_WARN_DEPRECATED
+ #define PLIST_WARN_DEPRECATED(x) __declspec(deprecated(x))
+ #endif
+#else
+ #define PLIST_WARN_DEPRECATED(x)
+ #pragma message("WARNING: You need to implement DEPRECATED for this compiler")
+#endif
+
+ /**
+ * \mainpage libplist : A library to handle Apple Property Lists
+ * \defgroup PublicAPI Public libplist API
+ */
+ /*@{*/
+
+
+ /**
+ * The basic plist abstract data type.
+ */
+ typedef void *plist_t;
+
+ /**
+ * The plist dictionary iterator.
+ */
+ typedef void* plist_dict_iter;
+
+ /**
+ * The plist array iterator.
+ */
+ typedef void* plist_array_iter;
+
+ /**
+ * The enumeration of plist node types.
+ */
+ typedef enum
+ {
+ PLIST_BOOLEAN, /**< Boolean, scalar type */
+ PLIST_INT, /**< Integer, scalar type */
+ PLIST_REAL, /**< Real, scalar type */
+ PLIST_STRING, /**< ASCII string, scalar type */
+ PLIST_ARRAY, /**< Ordered array, structured type */
+ PLIST_DICT, /**< Unordered dictionary (key/value pair), structured type */
+ PLIST_DATE, /**< Date, scalar type */
+ PLIST_DATA, /**< Binary data, scalar type */
+ PLIST_KEY, /**< Key in dictionaries (ASCII String), scalar type */
+ PLIST_UID, /**< Special type used for 'keyed encoding' */
+ PLIST_NULL, /**< NULL type */
+ PLIST_NONE /**< No type */
+ } plist_type;
+
+ /* for backwards compatibility */
+ #define PLIST_UINT PLIST_INT
+
+ /**
+ * libplist error values
+ */
+ typedef enum
+ {
+ PLIST_ERR_SUCCESS = 0, /**< operation successful */
+ PLIST_ERR_INVALID_ARG = -1, /**< one or more of the parameters are invalid */
+ PLIST_ERR_FORMAT = -2, /**< the plist contains nodes not compatible with the output format */
+ PLIST_ERR_PARSE = -3, /**< parsing of the input format failed */
+ PLIST_ERR_NO_MEM = -4, /**< not enough memory to handle the operation */
+ PLIST_ERR_UNKNOWN = -255 /**< an unspecified error occurred */
+ } plist_err_t;
+
+ /********************************************
+ * *
+ * Creation & Destruction *
+ * *
+ ********************************************/
+
+ /**
+ * Create a new root plist_t type #PLIST_DICT
+ *
+ * @return the created plist
+ * @sa #plist_type
+ */
+ plist_t plist_new_dict(void);
+
+ /**
+ * Create a new root plist_t type #PLIST_ARRAY
+ *
+ * @return the created plist
+ * @sa #plist_type
+ */
+ plist_t plist_new_array(void);
+
+ /**
+ * Create a new plist_t type #PLIST_STRING
+ *
+ * @param val the sting value, encoded in UTF8.
+ * @return the created item
+ * @sa #plist_type
+ */
+ plist_t plist_new_string(const char *val);
+
+ /**
+ * Create a new plist_t type #PLIST_BOOLEAN
+ *
+ * @param val the boolean value, 0 is false, other values are true.
+ * @return the created item
+ * @sa #plist_type
+ */
+ plist_t plist_new_bool(uint8_t val);
+
+ /**
+ * Create a new plist_t type #PLIST_INT with an unsigned integer value
+ *
+ * @param val the unsigned integer value
+ * @return the created item
+ * @sa #plist_type
+ * @note The value is always stored as uint64_t internally.
+ * Use #plist_get_uint_val or #plist_get_int_val to get the unsigned or signed value.
+ */
+ plist_t plist_new_uint(uint64_t val);
+
+ /**
+ * Create a new plist_t type #PLIST_INT with a signed integer value
+ *
+ * @param val the signed integer value
+ * @return the created item
+ * @sa #plist_type
+ * @note The value is always stored as uint64_t internally.
+ * Use #plist_get_uint_val or #plist_get_int_val to get the unsigned or signed value.
+ */
+ plist_t plist_new_int(int64_t val);
+
+ /**
+ * Create a new plist_t type #PLIST_REAL
+ *
+ * @param val the real value
+ * @return the created item
+ * @sa #plist_type
+ */
+ plist_t plist_new_real(double val);
+
+ /**
+ * Create a new plist_t type #PLIST_DATA
+ *
+ * @param val the binary buffer
+ * @param length the length of the buffer
+ * @return the created item
+ * @sa #plist_type
+ */
+ plist_t plist_new_data(const char *val, uint64_t length);
+
+ /**
+ * Create a new plist_t type #PLIST_DATE
+ *
+ * @param sec the number of seconds since 01/01/2001
+ * @param usec the number of microseconds
+ * @return the created item
+ * @sa #plist_type
+ */
+ plist_t plist_new_date(int32_t sec, int32_t usec);
+
+ /**
+ * Create a new plist_t type #PLIST_UID
+ *
+ * @param val the unsigned integer value
+ * @return the created item
+ * @sa #plist_type
+ */
+ plist_t plist_new_uid(uint64_t val);
+
+ /**
+ * Create a new plist_t type #PLIST_NULL
+ * @return the created item
+ * @sa #plist_type
+ * @note This type is not valid for all formats, e.g. the XML format
+ * does not support it.
+ */
+ plist_t plist_new_null(void);
+
+ /**
+ * Destruct a plist_t node and all its children recursively
+ *
+ * @param plist the plist to free
+ */
+ void plist_free(plist_t plist);
+
+ /**
+ * Return a copy of passed node and it's children
+ *
+ * @param node the plist to copy
+ * @return copied plist
+ */
+ plist_t plist_copy(plist_t node);
+
+
+ /********************************************
+ * *
+ * Array functions *
+ * *
+ ********************************************/
+
+ /**
+ * Get size of a #PLIST_ARRAY node.
+ *
+ * @param node the node of type #PLIST_ARRAY
+ * @return size of the #PLIST_ARRAY node
+ */
+ uint32_t plist_array_get_size(plist_t node);
+
+ /**
+ * Get the nth item in a #PLIST_ARRAY node.
+ *
+ * @param node the node of type #PLIST_ARRAY
+ * @param n the index of the item to get. Range is [0, array_size[
+ * @return the nth item or NULL if node is not of type #PLIST_ARRAY
+ */
+ plist_t plist_array_get_item(plist_t node, uint32_t n);
+
+ /**
+ * Get the index of an item. item must be a member of a #PLIST_ARRAY node.
+ *
+ * @param node the node
+ * @return the node index or UINT_MAX if node index can't be determined
+ */
+ uint32_t plist_array_get_item_index(plist_t node);
+
+ /**
+ * Set the nth item in a #PLIST_ARRAY node.
+ * The previous item at index n will be freed using #plist_free
+ *
+ * @param node the node of type #PLIST_ARRAY
+ * @param item the new item at index n. The array is responsible for freeing item when it is no longer needed.
+ * @param n the index of the item to get. Range is [0, array_size[. Assert if n is not in range.
+ */
+ void plist_array_set_item(plist_t node, plist_t item, uint32_t n);
+
+ /**
+ * Append a new item at the end of a #PLIST_ARRAY node.
+ *
+ * @param node the node of type #PLIST_ARRAY
+ * @param item the new item. The array is responsible for freeing item when it is no longer needed.
+ */
+ void plist_array_append_item(plist_t node, plist_t item);
+
+ /**
+ * Insert a new item at position n in a #PLIST_ARRAY node.
+ *
+ * @param node the node of type #PLIST_ARRAY
+ * @param item the new item to insert. The array is responsible for freeing item when it is no longer needed.
+ * @param n The position at which the node will be stored. Range is [0, array_size[. Assert if n is not in range.
+ */
+ void plist_array_insert_item(plist_t node, plist_t item, uint32_t n);
+
+ /**
+ * Remove an existing position in a #PLIST_ARRAY node.
+ * Removed position will be freed using #plist_free.
+ *
+ * @param node the node of type #PLIST_ARRAY
+ * @param n The position to remove. Range is [0, array_size[. Assert if n is not in range.
+ */
+ void plist_array_remove_item(plist_t node, uint32_t n);
+
+ /**
+ * Remove a node that is a child node of a #PLIST_ARRAY node.
+ * node will be freed using #plist_free.
+ *
+ * @param node The node to be removed from its #PLIST_ARRAY parent.
+ */
+ void plist_array_item_remove(plist_t node);
+
+ /**
+ * Create an iterator of a #PLIST_ARRAY node.
+ * The allocated iterator should be freed with the standard free function.
+ *
+ * @param node The node of type #PLIST_ARRAY
+ * @param iter Location to store the iterator for the array.
+ */
+ void plist_array_new_iter(plist_t node, plist_array_iter *iter);
+
+ /**
+ * Increment iterator of a #PLIST_ARRAY node.
+ *
+ * @param node The node of type #PLIST_ARRAY.
+ * @param iter Iterator of the array
+ * @param item Location to store the item. The caller must *not* free the
+ * returned item. Will be set to NULL when no more items are left
+ * to iterate.
+ */
+ void plist_array_next_item(plist_t node, plist_array_iter iter, plist_t *item);
+
+
+ /********************************************
+ * *
+ * Dictionary functions *
+ * *
+ ********************************************/
+
+ /**
+ * Get size of a #PLIST_DICT node.
+ *
+ * @param node the node of type #PLIST_DICT
+ * @return size of the #PLIST_DICT node
+ */
+ uint32_t plist_dict_get_size(plist_t node);
+
+ /**
+ * Create an iterator of a #PLIST_DICT node.
+ * The allocated iterator should be freed with the standard free function.
+ *
+ * @param node The node of type #PLIST_DICT.
+ * @param iter Location to store the iterator for the dictionary.
+ */
+ void plist_dict_new_iter(plist_t node, plist_dict_iter *iter);
+
+ /**
+ * Increment iterator of a #PLIST_DICT node.
+ *
+ * @param node The node of type #PLIST_DICT
+ * @param iter Iterator of the dictionary
+ * @param key Location to store the key, or NULL. The caller is responsible
+ * for freeing the the returned string.
+ * @param val Location to store the value, or NULL. The caller must *not*
+ * free the returned value. Will be set to NULL when no more
+ * key/value pairs are left to iterate.
+ */
+ void plist_dict_next_item(plist_t node, plist_dict_iter iter, char **key, plist_t *val);
+
+ /**
+ * Get key associated key to an item. Item must be member of a dictionary.
+ *
+ * @param node the item
+ * @param key a location to store the key. The caller is responsible for freeing the returned string.
+ */
+ void plist_dict_get_item_key(plist_t node, char **key);
+
+ /**
+ * Get the nth item in a #PLIST_DICT node.
+ *
+ * @param node the node of type #PLIST_DICT
+ * @param key the identifier of the item to get.
+ * @return the item or NULL if node is not of type #PLIST_DICT. The caller should not free
+ * the returned node.
+ */
+ plist_t plist_dict_get_item(plist_t node, const char* key);
+
+ /**
+ * Get key node associated to an item. Item must be member of a dictionary.
+ *
+ * @param node the item
+ * @return the key node of the given item, or NULL.
+ */
+ plist_t plist_dict_item_get_key(plist_t node);
+
+ /**
+ * Set item identified by key in a #PLIST_DICT node.
+ * The previous item identified by key will be freed using #plist_free.
+ * If there is no item for the given key a new item will be inserted.
+ *
+ * @param node the node of type #PLIST_DICT
+ * @param item the new item associated to key
+ * @param key the identifier of the item to set.
+ */
+ void plist_dict_set_item(plist_t node, const char* key, plist_t item);
+
+ /**
+ * Insert a new item into a #PLIST_DICT node.
+ *
+ * @deprecated Deprecated. Use plist_dict_set_item instead.
+ *
+ * @param node the node of type #PLIST_DICT
+ * @param item the new item to insert
+ * @param key The identifier of the item to insert.
+ */
+ PLIST_WARN_DEPRECATED("use plist_dict_set_item instead")
+ void plist_dict_insert_item(plist_t node, const char* key, plist_t item);
+
+ /**
+ * Remove an existing position in a #PLIST_DICT node.
+ * Removed position will be freed using #plist_free
+ *
+ * @param node the node of type #PLIST_DICT
+ * @param key The identifier of the item to remove. Assert if identifier is not present.
+ */
+ void plist_dict_remove_item(plist_t node, const char* key);
+
+ /**
+ * Merge a dictionary into another. This will add all key/value pairs
+ * from the source dictionary to the target dictionary, overwriting
+ * any existing key/value pairs that are already present in target.
+ *
+ * @param target pointer to an existing node of type #PLIST_DICT
+ * @param source node of type #PLIST_DICT that should be merged into target
+ */
+ void plist_dict_merge(plist_t *target, plist_t source);
+
+
+ /********************************************
+ * *
+ * Getters *
+ * *
+ ********************************************/
+
+ /**
+ * Get the parent of a node
+ *
+ * @param node the parent (NULL if node is root)
+ */
+ plist_t plist_get_parent(plist_t node);
+
+ /**
+ * Get the #plist_type of a node.
+ *
+ * @param node the node
+ * @return the type of the node
+ */
+ plist_type plist_get_node_type(plist_t node);
+
+ /**
+ * Get the value of a #PLIST_KEY node.
+ * This function does nothing if node is not of type #PLIST_KEY
+ *
+ * @param node the node
+ * @param val a pointer to a C-string. This function allocates the memory,
+ * caller is responsible for freeing it.
+ * @note Use plist_mem_free() to free the allocated memory.
+ */
+ void plist_get_key_val(plist_t node, char **val);
+
+ /**
+ * Get the value of a #PLIST_STRING node.
+ * This function does nothing if node is not of type #PLIST_STRING
+ *
+ * @param node the node
+ * @param val a pointer to a C-string. This function allocates the memory,
+ * caller is responsible for freeing it. Data is UTF-8 encoded.
+ * @note Use plist_mem_free() to free the allocated memory.
+ */
+ void plist_get_string_val(plist_t node, char **val);
+
+ /**
+ * Get a pointer to the buffer of a #PLIST_STRING node.
+ *
+ * @note DO NOT MODIFY the buffer. Mind that the buffer is only available
+ * until the plist node gets freed. Make a copy if needed.
+ *
+ * @param node The node
+ * @param length If non-NULL, will be set to the length of the string
+ *
+ * @return Pointer to the NULL-terminated buffer.
+ */
+ const char* plist_get_string_ptr(plist_t node, uint64_t* length);
+
+ /**
+ * Get the value of a #PLIST_BOOLEAN node.
+ * This function does nothing if node is not of type #PLIST_BOOLEAN
+ *
+ * @param node the node
+ * @param val a pointer to a uint8_t variable.
+ */
+ void plist_get_bool_val(plist_t node, uint8_t * val);
+
+ /**
+ * Get the unsigned integer value of a #PLIST_INT node.
+ * This function does nothing if node is not of type #PLIST_INT
+ *
+ * @param node the node
+ * @param val a pointer to a uint64_t variable.
+ */
+ void plist_get_uint_val(plist_t node, uint64_t * val);
+
+ /**
+ * Get the signed integer value of a #PLIST_INT node.
+ * This function does nothing if node is not of type #PLIST_INT
+ *
+ * @param node the node
+ * @param val a pointer to a int64_t variable.
+ */
+ void plist_get_int_val(plist_t node, int64_t * val);
+
+ /**
+ * Get the value of a #PLIST_REAL node.
+ * This function does nothing if node is not of type #PLIST_REAL
+ *
+ * @param node the node
+ * @param val a pointer to a double variable.
+ */
+ void plist_get_real_val(plist_t node, double *val);
+
+ /**
+ * Get the value of a #PLIST_DATA node.
+ * This function does nothing if node is not of type #PLIST_DATA
+ *
+ * @param node the node
+ * @param val a pointer to an unallocated char buffer. This function allocates the memory,
+ * caller is responsible for freeing it.
+ * @param length the length of the buffer
+ * @note Use plist_mem_free() to free the allocated memory.
+ */
+ void plist_get_data_val(plist_t node, char **val, uint64_t * length);
+
+ /**
+ * Get a pointer to the data buffer of a #PLIST_DATA node.
+ *
+ * @note DO NOT MODIFY the buffer. Mind that the buffer is only available
+ * until the plist node gets freed. Make a copy if needed.
+ *
+ * @param node The node
+ * @param length Pointer to a uint64_t that will be set to the length of the buffer
+ *
+ * @return Pointer to the buffer
+ */
+ const char* plist_get_data_ptr(plist_t node, uint64_t* length);
+
+ /**
+ * Get the value of a #PLIST_DATE node.
+ * This function does nothing if node is not of type #PLIST_DATE
+ *
+ * @param node the node
+ * @param sec a pointer to an int32_t variable. Represents the number of seconds since 01/01/2001.
+ * @param usec a pointer to an int32_t variable. Represents the number of microseconds
+ */
+ void plist_get_date_val(plist_t node, int32_t * sec, int32_t * usec);
+
+ /**
+ * Get the value of a #PLIST_UID node.
+ * This function does nothing if node is not of type #PLIST_UID
+ *
+ * @param node the node
+ * @param val a pointer to a uint64_t variable.
+ */
+ void plist_get_uid_val(plist_t node, uint64_t * val);
+
+
+ /********************************************
+ * *
+ * Setters *
+ * *
+ ********************************************/
+
+ /**
+ * Set the value of a node.
+ * Forces type of node to #PLIST_KEY
+ *
+ * @param node the node
+ * @param val the key value
+ */
+ void plist_set_key_val(plist_t node, const char *val);
+
+ /**
+ * Set the value of a node.
+ * Forces type of node to #PLIST_STRING
+ *
+ * @param node the node
+ * @param val the string value. The string is copied when set and will be
+ * freed by the node.
+ */
+ void plist_set_string_val(plist_t node, const char *val);
+
+ /**
+ * Set the value of a node.
+ * Forces type of node to #PLIST_BOOLEAN
+ *
+ * @param node the node
+ * @param val the boolean value
+ */
+ void plist_set_bool_val(plist_t node, uint8_t val);
+
+ /**
+ * Set the value of a node.
+ * Forces type of node to #PLIST_INT
+ *
+ * @param node the node
+ * @param val the unsigned integer value
+ */
+ void plist_set_uint_val(plist_t node, uint64_t val);
+
+ /**
+ * Set the value of a node.
+ * Forces type of node to #PLIST_INT
+ *
+ * @param node the node
+ * @param val the signed integer value
+ */
+ void plist_set_int_val(plist_t node, int64_t val);
+
+ /**
+ * Set the value of a node.
+ * Forces type of node to #PLIST_REAL
+ *
+ * @param node the node
+ * @param val the real value
+ */
+ void plist_set_real_val(plist_t node, double val);
+
+ /**
+ * Set the value of a node.
+ * Forces type of node to #PLIST_DATA
+ *
+ * @param node the node
+ * @param val the binary buffer. The buffer is copied when set and will
+ * be freed by the node.
+ * @param length the length of the buffer
+ */
+ void plist_set_data_val(plist_t node, const char *val, uint64_t length);
+
+ /**
+ * Set the value of a node.
+ * Forces type of node to #PLIST_DATE
+ *
+ * @param node the node
+ * @param sec the number of seconds since 01/01/2001
+ * @param usec the number of microseconds
+ */
+ void plist_set_date_val(plist_t node, int32_t sec, int32_t usec);
+
+ /**
+ * Set the value of a node.
+ * Forces type of node to #PLIST_UID
+ *
+ * @param node the node
+ * @param val the unsigned integer value
+ */
+ void plist_set_uid_val(plist_t node, uint64_t val);
+
+
+ /********************************************
+ * *
+ * Import & Export *
+ * *
+ ********************************************/
+
+ /**
+ * Export the #plist_t structure to XML format.
+ *
+ * @param plist the root node to export
+ * @param plist_xml a pointer to a C-string. This function allocates the memory,
+ * caller is responsible for freeing it. Data is UTF-8 encoded.
+ * @param length a pointer to an uint32_t variable. Represents the length of the allocated buffer.
+ * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+ * @note Use plist_mem_free() to free the allocated memory.
+ */
+ plist_err_t plist_to_xml(plist_t plist, char **plist_xml, uint32_t * length);
+
+ /**
+ * Export the #plist_t structure to binary format.
+ *
+ * @param plist the root node to export
+ * @param plist_bin a pointer to a char* buffer. This function allocates the memory,
+ * caller is responsible for freeing it.
+ * @param length a pointer to an uint32_t variable. Represents the length of the allocated buffer.
+ * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+ * @note Use plist_mem_free() to free the allocated memory.
+ */
+ plist_err_t plist_to_bin(plist_t plist, char **plist_bin, uint32_t * length);
+
+ /**
+ * Export the #plist_t structure to JSON format.
+ *
+ * @param plist the root node to export
+ * @param plist_json a pointer to a char* buffer. This function allocates the memory,
+ * caller is responsible for freeing it.
+ * @param length a pointer to an uint32_t variable. Represents the length of the allocated buffer.
+ * @param prettify pretty print the output if != 0
+ * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+ * @note Use plist_mem_free() to free the allocated memory.
+ */
+ plist_err_t plist_to_json(plist_t plist, char **plist_json, uint32_t* length, int prettify);
+
+ /**
+ * Export the #plist_t structure to OpenStep format.
+ *
+ * @param plist the root node to export
+ * @param plist_openstep a pointer to a char* buffer. This function allocates the memory,
+ * caller is responsible for freeing it.
+ * @param length a pointer to an uint32_t variable. Represents the length of the allocated buffer.
+ * @param prettify pretty print the output if != 0
+ * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+ * @note Use plist_mem_free() to free the allocated memory.
+ */
+ plist_err_t plist_to_openstep(plist_t plist, char **plist_openstep, uint32_t* length, int prettify);
+
+
+ /**
+ * Import the #plist_t structure from XML format.
+ *
+ * @param plist_xml a pointer to the xml buffer.
+ * @param length length of the buffer to read.
+ * @param plist a pointer to the imported plist.
+ * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+ */
+ plist_err_t plist_from_xml(const char *plist_xml, uint32_t length, plist_t * plist);
+
+ /**
+ * Import the #plist_t structure from binary format.
+ *
+ * @param plist_bin a pointer to the xml buffer.
+ * @param length length of the buffer to read.
+ * @param plist a pointer to the imported plist.
+ * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+ */
+ plist_err_t plist_from_bin(const char *plist_bin, uint32_t length, plist_t * plist);
+
+ /**
+ * Import the #plist_t structure from JSON format.
+ *
+ * @param json a pointer to the JSON buffer.
+ * @param length length of the buffer to read.
+ * @param plist a pointer to the imported plist.
+ * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+ */
+ plist_err_t plist_from_json(const char *json, uint32_t length, plist_t * plist);
+
+ /**
+ * Import the #plist_t structure from OpenStep plist format.
+ *
+ * @param openstep a pointer to the OpenStep plist buffer.
+ * @param length length of the buffer to read.
+ * @param plist a pointer to the imported plist.
+ * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+ */
+ plist_err_t plist_from_openstep(const char *openstep, uint32_t length, plist_t * plist);
+
+ /**
+ * Import the #plist_t structure from memory data.
+ * This method will look at the first bytes of plist_data
+ * to determine if plist_data contains a binary, JSON, or XML plist
+ * and tries to parse the data in the appropriate format.
+ * @note This is just a convenience function and the format detection is
+ * very basic. It checks with plist_is_binary() if the data supposedly
+ * contains binary plist data, if not it checks if the first byte is
+ * either '{' or '[' and assumes JSON format, otherwise it will try
+ * to parse the data as XML.
+ *
+ * @param plist_data a pointer to the memory buffer containing plist data.
+ * @param length length of the buffer to read.
+ * @param plist a pointer to the imported plist.
+ * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+ */
+ plist_err_t plist_from_memory(const char *plist_data, uint32_t length, plist_t * plist);
+
+ /**
+ * Test if in-memory plist data is in binary format.
+ * This function will look at the first bytes of plist_data to determine
+ * if it supposedly contains a binary plist.
+ * @note The function is not validating the whole memory buffer to check
+ * if the content is truly a plist, it is only using some heuristic on
+ * the first few bytes of plist_data.
+ *
+ * @param plist_data a pointer to the memory buffer containing plist data.
+ * @param length length of the buffer to read.
+ * @return 1 if the buffer is a binary plist, 0 otherwise.
+ */
+ int plist_is_binary(const char *plist_data, uint32_t length);
+
+ /********************************************
+ * *
+ * Utils *
+ * *
+ ********************************************/
+
+ /**
+ * Get a node from its path. Each path element depends on the associated father node type.
+ * For Dictionaries, var args are casted to const char*, for arrays, var args are caster to uint32_t
+ * Search is breath first order.
+ *
+ * @param plist the node to access result from.
+ * @param length length of the path to access
+ * @return the value to access.
+ */
+ plist_t plist_access_path(plist_t plist, uint32_t length, ...);
+
+ /**
+ * Variadic version of #plist_access_path.
+ *
+ * @param plist the node to access result from.
+ * @param length length of the path to access
+ * @param v list of array's index and dic'st key
+ * @return the value to access.
+ */
+ plist_t plist_access_pathv(plist_t plist, uint32_t length, va_list v);
+
+ /**
+ * Compare two node values
+ *
+ * @param node_l left node to compare
+ * @param node_r rigth node to compare
+ * @return TRUE is type and value match, FALSE otherwise.
+ */
+ char plist_compare_node_value(plist_t node_l, plist_t node_r);
+
+ #define _PLIST_IS_TYPE(__plist, __plist_type) (__plist && (plist_get_node_type(__plist) == PLIST_##__plist_type))
+
+ /* Helper macros for the different plist types */
+ #define PLIST_IS_BOOLEAN(__plist) _PLIST_IS_TYPE(__plist, BOOLEAN)
+ #define PLIST_IS_INT(__plist) _PLIST_IS_TYPE(__plist, INT)
+ #define PLIST_IS_REAL(__plist) _PLIST_IS_TYPE(__plist, REAL)
+ #define PLIST_IS_STRING(__plist) _PLIST_IS_TYPE(__plist, STRING)
+ #define PLIST_IS_ARRAY(__plist) _PLIST_IS_TYPE(__plist, ARRAY)
+ #define PLIST_IS_DICT(__plist) _PLIST_IS_TYPE(__plist, DICT)
+ #define PLIST_IS_DATE(__plist) _PLIST_IS_TYPE(__plist, DATE)
+ #define PLIST_IS_DATA(__plist) _PLIST_IS_TYPE(__plist, DATA)
+ #define PLIST_IS_KEY(__plist) _PLIST_IS_TYPE(__plist, KEY)
+ #define PLIST_IS_UID(__plist) _PLIST_IS_TYPE(__plist, UID)
+ /* for backwards compatibility */
+ #define PLIST_IS_UINT PLIST_IS_INT
+
+ /**
+ * Helper function to check the value of a PLIST_BOOL node.
+ *
+ * @param boolnode node of type PLIST_BOOL
+ * @return 1 if the boolean node has a value of TRUE or 0 if FALSE.
+ */
+ int plist_bool_val_is_true(plist_t boolnode);
+
+ /**
+ * Helper function to test if a given #PLIST_INT node's value is negative
+ *
+ * @param intnode node of type PLIST_INT
+ * @return 1 if the node's value is negative, or 0 if positive.
+ */
+ int plist_int_val_is_negative(plist_t intnode);
+
+ /**
+ * Helper function to compare the value of a PLIST_INT node against
+ * a given signed integer value.
+ *
+ * @param uintnode node of type PLIST_INT
+ * @param cmpval value to compare against
+ * @return 0 if the node's value and cmpval are equal,
+ * 1 if the node's value is greater than cmpval,
+ * or -1 if the node's value is less than cmpval.
+ */
+ int plist_int_val_compare(plist_t uintnode, int64_t cmpval);
+
+ /**
+ * Helper function to compare the value of a PLIST_INT node against
+ * a given unsigned integer value.
+ *
+ * @param uintnode node of type PLIST_INT
+ * @param cmpval value to compare against
+ * @return 0 if the node's value and cmpval are equal,
+ * 1 if the node's value is greater than cmpval,
+ * or -1 if the node's value is less than cmpval.
+ */
+ int plist_uint_val_compare(plist_t uintnode, uint64_t cmpval);
+
+ /**
+ * Helper function to compare the value of a PLIST_UID node against
+ * a given value.
+ *
+ * @param uidnode node of type PLIST_UID
+ * @param cmpval value to compare against
+ * @return 0 if the node's value and cmpval are equal,
+ * 1 if the node's value is greater than cmpval,
+ * or -1 if the node's value is less than cmpval.
+ */
+ int plist_uid_val_compare(plist_t uidnode, uint64_t cmpval);
+
+ /**
+ * Helper function to compare the value of a PLIST_REAL node against
+ * a given value.
+ *
+ * @note WARNING: Comparing floating point values can give inaccurate
+ * results because of the nature of floating point values on computer
+ * systems. While this function is designed to be as accurate as
+ * possible, please don't rely on it too much.
+ *
+ * @param realnode node of type PLIST_REAL
+ * @param cmpval value to compare against
+ * @return 0 if the node's value and cmpval are (almost) equal,
+ * 1 if the node's value is greater than cmpval,
+ * or -1 if the node's value is less than cmpval.
+ */
+ int plist_real_val_compare(plist_t realnode, double cmpval);
+
+ /**
+ * Helper function to compare the value of a PLIST_DATE node against
+ * a given set of seconds and fraction of a second since epoch.
+ *
+ * @param datenode node of type PLIST_DATE
+ * @param cmpsec number of seconds since epoch to compare against
+ * @param cmpusec fraction of a second in microseconds to compare against
+ * @return 0 if the node's date is equal to the supplied values,
+ * 1 if the node's date is greater than the supplied values,
+ * or -1 if the node's date is less than the supplied values.
+ */
+ int plist_date_val_compare(plist_t datenode, int32_t cmpsec, int32_t cmpusec);
+
+ /**
+ * Helper function to compare the value of a PLIST_STRING node against
+ * a given value.
+ * This function basically behaves like strcmp.
+ *
+ * @param strnode node of type PLIST_STRING
+ * @param cmpval value to compare against
+ * @return 0 if the node's value and cmpval are equal,
+ * > 0 if the node's value is lexicographically greater than cmpval,
+ * or < 0 if the node's value is lexicographically less than cmpval.
+ */
+ int plist_string_val_compare(plist_t strnode, const char* cmpval);
+
+ /**
+ * Helper function to compare the value of a PLIST_STRING node against
+ * a given value, while not comparing more than n characters.
+ * This function basically behaves like strncmp.
+ *
+ * @param strnode node of type PLIST_STRING
+ * @param cmpval value to compare against
+ * @param n maximum number of characters to compare
+ * @return 0 if the node's value and cmpval are equal,
+ * > 0 if the node's value is lexicographically greater than cmpval,
+ * or < 0 if the node's value is lexicographically less than cmpval.
+ */
+ int plist_string_val_compare_with_size(plist_t strnode, const char* cmpval, size_t n);
+
+ /**
+ * Helper function to match a given substring in the value of a
+ * PLIST_STRING node.
+ *
+ * @param strnode node of type PLIST_STRING
+ * @param substr value to match
+ * @return 1 if the node's value contains the given substring,
+ * or 0 if not.
+ */
+ int plist_string_val_contains(plist_t strnode, const char* substr);
+
+ /**
+ * Helper function to compare the value of a PLIST_KEY node against
+ * a given value.
+ * This function basically behaves like strcmp.
+ *
+ * @param keynode node of type PLIST_KEY
+ * @param cmpval value to compare against
+ * @return 0 if the node's value and cmpval are equal,
+ * > 0 if the node's value is lexicographically greater than cmpval,
+ * or < 0 if the node's value is lexicographically less than cmpval.
+ */
+ int plist_key_val_compare(plist_t keynode, const char* cmpval);
+
+ /**
+ * Helper function to compare the value of a PLIST_KEY node against
+ * a given value, while not comparing more than n characters.
+ * This function basically behaves like strncmp.
+ *
+ * @param keynode node of type PLIST_KEY
+ * @param cmpval value to compare against
+ * @param n maximum number of characters to compare
+ * @return 0 if the node's value and cmpval are equal,
+ * > 0 if the node's value is lexicographically greater than cmpval,
+ * or < 0 if the node's value is lexicographically less than cmpval.
+ */
+ int plist_key_val_compare_with_size(plist_t keynode, const char* cmpval, size_t n);
+
+ /**
+ * Helper function to match a given substring in the value of a
+ * PLIST_KEY node.
+ *
+ * @param keynode node of type PLIST_KEY
+ * @param substr value to match
+ * @return 1 if the node's value contains the given substring,
+ * or 0 if not.
+ */
+ int plist_key_val_contains(plist_t keynode, const char* substr);
+
+ /**
+ * Helper function to compare the data of a PLIST_DATA node against
+ * a given blob and size.
+ * This function basically behaves like memcmp after making sure the
+ * size of the node's data value is equal to the size of cmpval (n),
+ * making this a "full match" comparison.
+ *
+ * @param datanode node of type PLIST_DATA
+ * @param cmpval data blob to compare against
+ * @param n size of data blob passed in cmpval
+ * @return 0 if the node's data blob and cmpval are equal,
+ * > 0 if the node's value is lexicographically greater than cmpval,
+ * or < 0 if the node's value is lexicographically less than cmpval.
+ */
+ int plist_data_val_compare(plist_t datanode, const uint8_t* cmpval, size_t n);
+
+ /**
+ * Helper function to compare the data of a PLIST_DATA node against
+ * a given blob and size, while no more than n bytes are compared.
+ * This function basically behaves like memcmp after making sure the
+ * size of the node's data value is at least n, making this a
+ * "starts with" comparison.
+ *
+ * @param datanode node of type PLIST_DATA
+ * @param cmpval data blob to compare against
+ * @param n size of data blob passed in cmpval
+ * @return 0 if the node's value and cmpval are equal,
+ * > 0 if the node's value is lexicographically greater than cmpval,
+ * or < 0 if the node's value is lexicographically less than cmpval.
+ */
+ int plist_data_val_compare_with_size(plist_t datanode, const uint8_t* cmpval, size_t n);
+
+ /**
+ * Helper function to match a given data blob within the value of a
+ * PLIST_DATA node.
+ *
+ * @param datanode node of type PLIST_KEY
+ * @param cmpval data blob to match
+ * @param n size of data blob passed in cmpval
+ * @return 1 if the node's value contains the given data blob
+ * or 0 if not.
+ */
+ int plist_data_val_contains(plist_t datanode, const uint8_t* cmpval, size_t n);
+
+ /**
+ * Free memory allocated by relevant libplist API calls:
+ * - plist_to_xml()
+ * - plist_to_bin()
+ * - plist_get_key_val()
+ * - plist_get_string_val()
+ * - plist_get_data_val()
+ *
+ * @param ptr pointer to the memory to free
+ *
+ * @note Do not use this function to free plist_t nodes, use plist_free()
+ * instead.
+ */
+ void plist_mem_free(void* ptr);
+
+ /*@}*/
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Info.plist b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Info.plist
new file mode 100644
index 000000000..228311e3f
Binary files /dev/null and b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Info.plist differ
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/StosJIT b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/StosJIT
new file mode 100755
index 000000000..4b24b750d
Binary files /dev/null and b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/StosJIT differ
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/_CodeSignature/CodeResources b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/_CodeSignature/CodeResources
new file mode 100644
index 000000000..8cc62f1fe
--- /dev/null
+++ b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/_CodeSignature/CodeResources
@@ -0,0 +1,205 @@
+
+
+
+
+ files
+
+ .DS_Store
+
+ 7Mfr8shT4pXWBr/plN+uNkIabdM=
+
+ Headers/StosJIT-Swift.h
+
+ h9vaTwhC6FlnyKmIkaxLQGlFd1g=
+
+ Headers/StosJIT.h
+
+ ggHr5wlLNIIPydwUL9Vxm6abxjo=
+
+ Headers/idevice.h
+
+ mHDz7368FsBID56/epJ2NgIkha4=
+
+ Headers/plist.h
+
+ bL/f0MQDpLfvIcI1zxPwMuJ/PfI=
+
+ Info.plist
+
+ ZTTwPKlta/gjXAr1HIHmyAxeU4E=
+
+ Modules/StosJIT.swiftmodule/Project/arm64-apple-ios.swiftsourceinfo
+
+ nihJghwM5m7kxkQD7UvrWyHkLy8=
+
+ Modules/StosJIT.swiftmodule/arm64-apple-ios.abi.json
+
+ gcwBsH4BgyFY4sVtNt+/xOKS3vY=
+
+ Modules/StosJIT.swiftmodule/arm64-apple-ios.swiftdoc
+
+ YPtkDrAuBiPPEp4ZdRdBVlFXnRM=
+
+ Modules/StosJIT.swiftmodule/arm64-apple-ios.swiftmodule
+
+ 9cIInnjJzJFtY+CZm2iNo5qL3MQ=
+
+ Modules/module.modulemap
+
+ cnpvYzvLIwWcxkQodj5uLbHkyRk=
+
+
+ files2
+
+ Headers/StosJIT-Swift.h
+
+ hash2
+
+ 1obIr4IjMvtcyNyYIV/Nh/5wahcA1cFjc4n4XVlNt2I=
+
+
+ Headers/StosJIT.h
+
+ hash2
+
+ yY9KyrRdOYRdlb7G6wVMU2hogasXMjwV5r8jUIk44ok=
+
+
+ Headers/idevice.h
+
+ hash2
+
+ zR9/TB9Dnv3uRC8qqGvaQ6c2yyOFUURmrHKLdEiUh/g=
+
+
+ Headers/plist.h
+
+ hash2
+
+ yFbGsiXBBp91tfsSFtS0Utt2Gpc3MEDFiMVXKG9q1rs=
+
+
+ Modules/StosJIT.swiftmodule/Project/arm64-apple-ios.swiftsourceinfo
+
+ hash2
+
+ +Ehvco7cQbAaF7zufvBYTiGXFp37Hjym/Pav514sGPk=
+
+
+ Modules/StosJIT.swiftmodule/arm64-apple-ios.abi.json
+
+ hash2
+
+ Qnesa0n4URGWAopawg9bGx36dUwkYV00BoCJ8LFzlyg=
+
+
+ Modules/StosJIT.swiftmodule/arm64-apple-ios.swiftdoc
+
+ hash2
+
+ k7F2Xs2hh9iMbK8IE8TMtN6gjQ9kWs30NUKHeupq6VE=
+
+
+ Modules/StosJIT.swiftmodule/arm64-apple-ios.swiftmodule
+
+ hash2
+
+ gMDYNHcBPCNwZw2A5mEUiCyYAS9VhtQG0z+/WqAUrOQ=
+
+
+ Modules/module.modulemap
+
+ hash2
+
+ FGwGKs5SNvpCyiIWiOP4eml9m2e3KISmtCJVtNnUnUc=
+
+
+
+ rules
+
+ ^.*
+
+ ^.*\.lproj/
+
+ optional
+
+ weight
+ 1000
+
+ ^.*\.lproj/locversion.plist$
+
+ omit
+
+ weight
+ 1100
+
+ ^Base\.lproj/
+
+ weight
+ 1010
+
+ ^version.plist$
+
+
+ rules2
+
+ .*\.dSYM($|/)
+
+ weight
+ 11
+
+ ^(.*/)?\.DS_Store$
+
+ omit
+
+ weight
+ 2000
+
+ ^.*
+
+ ^.*\.lproj/
+
+ optional
+
+ weight
+ 1000
+
+ ^.*\.lproj/locversion.plist$
+
+ omit
+
+ weight
+ 1100
+
+ ^Base\.lproj/
+
+ weight
+ 1010
+
+ ^Info\.plist$
+
+ omit
+
+ weight
+ 20
+
+ ^PkgInfo$
+
+ omit
+
+ weight
+ 20
+
+ ^embedded\.provisionprofile$
+
+ weight
+ 20
+
+ ^version\.plist$
+
+ weight
+ 20
+
+
+
+
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libMoltenVK.dylib b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libMoltenVK.dylib
index c1b12b543..8996b7ca5 100755
Binary files a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libMoltenVK.dylib and b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libMoltenVK.dylib differ
diff --git a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/Info.plist b/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/Info.plist
deleted file mode 100644
index d0cb291b6..000000000
--- a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/Info.plist
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
- AvailableLibraries
-
-
- BinaryPath
- MoltenVK.framework/MoltenVK
- LibraryIdentifier
- ios-arm64
- LibraryPath
- MoltenVK.framework
- SupportedArchitectures
-
- arm64
-
- SupportedPlatform
- ios
-
-
- CFBundlePackageType
- XFWK
- XCFrameworkFormatVersion
- 1.0
-
-
diff --git a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/Info.plist b/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/Info.plist
deleted file mode 100644
index 2e0914e03..000000000
Binary files a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/Info.plist and /dev/null differ
diff --git a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/MoltenVK b/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/MoltenVK
deleted file mode 100755
index 495d9fb19..000000000
Binary files a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/MoltenVK and /dev/null differ
diff --git a/src/MeloNX/MeloNX/Info.plist b/src/MeloNX/MeloNX/Info.plist
index bb9bd4980..8c026ae5d 100644
--- a/src/MeloNX/MeloNX/Info.plist
+++ b/src/MeloNX/MeloNX/Info.plist
@@ -38,8 +38,9 @@
UIBackgroundModes
- audio
+ location
processing
+ audio
UIFileSharingEnabled
diff --git a/src/MeloNX/MeloNX/MeloNXApp.swift b/src/MeloNX/MeloNX/MeloNXApp.swift
deleted file mode 100644
index a8105777b..000000000
--- a/src/MeloNX/MeloNX/MeloNXApp.swift
+++ /dev/null
@@ -1,263 +0,0 @@
-//
-// MeloNXApp.swift
-// MeloNX
-//
-// Created by Stossy11 on 3/11/2024.
-//
-
-import SwiftUI
-import UIKit
-import CryptoKit
-
-
-
-@main
-struct MeloNXApp: App {
-
- @State var showed = false
- @Environment(\.scenePhase) var scenePhase
- @State var alert: UIAlertController? = nil
-
- var body: some Scene {
- WindowGroup {
- ZStack {
- if showed || DRM != 1 {
- ContentView()
- } else {
- Group {
- VStack {
- Spacer()
-
- HStack {
- Text("Loading...")
- ProgressView()
- }
- Spacer()
-
- Text(UIDevice.current.identifierForVendor?.uuidString ?? "")
- }
- }
- .onAppear {
- initR()
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- .background(Color.black.opacity(1))
- .foregroundColor(.white)
- }
- }
- }
- }
-
- func initR() {
- if DRM == 1 {
- DispatchQueue.main.async { [self] in
- // drmcheck()
- InitializeRyujinx() { bool in
- if bool {
- print("Ryujinx Files Initialized Successfully")
- DispatchQueue.main.async { [self] in
- withAnimation {
- showed = true
- }
-
- Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
- InitializeRyujinx() { bool in
- if !bool, (scenePhase != .background || scenePhase == .inactive) {
- withAnimation {
- showed = false
- }
- if !(alert?.isViewLoaded ?? false) {
- alert = showDMCAAlert()
- }
- } else {
- DispatchQueue.main.async {
- alert?.dismiss(animated: true)
- showed = true
- }
- }
- }
- }
-
- }
-
- } else {
- showDMCAAlert()
- }
-
- }
-
- }
-
- }
-
- }
-
-
- func showAlert() -> UIAlertController? {
- // Create the alert controller
- if let mainWindow = UIApplication.shared.windows.last {
- let alertController = UIAlertController(title: "Enter license", message: "Enter license key:", preferredStyle: .alert)
-
- // Add a text field to the alert
- alertController.addTextField { textField in
- textField.placeholder = "Enter key here"
- }
-
- // Add the "OK" action
- let okAction = UIAlertAction(title: "OK", style: .default) { _ in
- // Get the text entered in the text field
- if let textField = alertController.textFields?.first, let enteredText = textField.text {
- print("Entered text: \(enteredText)")
- UserDefaults.standard.set(enteredText, forKey: "MeloDRMID")
- // drmcheck() { bool in
- // if bool {
- // showed = true
- // } else {
- // exit(0)
- // }
- // }
- }
- }
- alertController.addAction(okAction)
-
- // Add a "Cancel" action
- let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
- alertController.addAction(cancelAction)
-
- // Present the alert
- mainWindow.rootViewController!.present(alertController, animated: true, completion: nil)
-
- return alertController
- } else {
- return nil
- }
- }
-
-
-}
-
-func showDMCAAlert() -> UIAlertController? {
- if let mainWindow = UIApplication.shared.windows.first {
- let alertController = UIAlertController(title: "Unauthorized Copy Notice", message: "This app was illegally leaked. Please report the download on the MeloNX Discord. In the meantime, check out Pomelo! \n -Stossy11", preferredStyle: .alert)
-
- DispatchQueue.main.async {
- mainWindow.rootViewController!.present(alertController, animated: true, completion: nil)
- }
-
- return alertController
- } else {
- // uhoh
- return nil
- }
-}
-
-/*
-func drmcheck(completion: @escaping (Bool) -> Void) {
- if let deviceid = UIDevice.current.identifierForVendor?.uuidString, let base64device = deviceid.data(using: .utf8)?.base64EncodedString() {
- if let value = UserDefaults.standard.string(forKey: "MeloDRMID") {
- if let url = URL(string: "https://mx.stossy11.com/auth/\(value)/\(base64device)") {
- print(url)
- // Create a URLSession
- let session = URLSession.shared
-
- // Create a data task
- let task = session.dataTask(with: url) { data, response, error in
- // Handle errors
- if let error = error {
- exit(0)
- }
-
- // Check response and data
- if let response = response as? HTTPURLResponse, response.statusCode == 200 {
- print("Successfully Recieved API Data")
- completion(true)
- } else if let response = response as? HTTPURLResponse, response.statusCode == 201 {
- print("Successfully Created Auth UUID")
- completion(true)
- } else {
- completion(false)
- }
- }
-
- // Start the task
- task.resume()
- }
- } else {
- completion(false)
- }
- } else {
- completion(false)
- }
-
-}
-*/
-
-func InitializeRyujinx(completion: @escaping (Bool) -> Void) {
- let path = "aHR0cHM6Ly9teC5zdG9zc3kxMS5jb20v"
-
- guard let value = Bundle.main.object(forInfoDictionaryKey: "MeloID") as? String, !value.isEmpty else {
- completion(false)
- return
- }
-
-
-
- if (detectRoms(path: path) != value) {
- completion(false)
- }
-
- let configuration = URLSessionConfiguration.default
- configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
- configuration.urlCache = nil
-
- let session = URLSession(configuration: configuration)
-
- guard let url = URL(string: addFolders(path)!) else {
- completion(false)
- return
- }
-
- let task = session.dataTask(with: url) { data, response, error in
- if error != nil {
- completion(false)
- }
-
-
- guard let httpResponse = response as? HTTPURLResponse else {
- completion(false)
- return
- }
-
- if httpResponse.statusCode == 200 {
- completion(true)
- } else {
- completion(false)
- }
- return
- }
- task.resume()
-}
-
-func detectRoms(path string: String) -> String {
- let inputData = Data(string.utf8)
- let romHash = SHA256.hash(data: inputData)
- return romHash.compactMap { String(format: "%02x", $0) }.joined()
-}
-
-
-
-func addFolders(_ folderPath: String) -> String? {
- let fileManager = FileManager.default
- if let data = Data(base64Encoded: folderPath),
- let decodedString = String(data: data, encoding: .utf8), let fileURL = UIDevice.current.identifierForVendor?.uuidString {
- return decodedString + "auth/" + fileURL + "/"
- }
- return nil
-}
-
-extension String {
-
- func print() {
- Swift.print(self)
- }
-}
diff --git a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs
index 744a4bc56..01286992f 100644
--- a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs
+++ b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs
@@ -20,6 +20,25 @@ namespace Ryujinx.Audio.Backends.OpenAL
private bool _stillRunning;
private readonly Thread _updaterThread;
+ private float _volume;
+
+ public float Volume
+ {
+ get
+ {
+ return _volume;
+ }
+ set
+ {
+ _volume = value;
+
+ foreach (OpenALHardwareDeviceSession session in _sessions.Keys)
+ {
+ session.UpdateMasterVolume(value);
+ }
+ }
+ }
+
public OpenALHardwareDeviceDriver()
{
_device = ALC.OpenDevice("");
@@ -34,6 +53,8 @@ namespace Ryujinx.Audio.Backends.OpenAL
Name = "HardwareDeviceDriver.OpenAL",
};
+ _volume = 1f;
+
_updaterThread.Start();
}
@@ -52,7 +73,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
}
}
- public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
+ public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (channelCount == 0)
{
@@ -73,7 +94,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
throw new ArgumentException($"{channelCount}");
}
- OpenALHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
+ OpenALHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
_sessions.TryAdd(session, 0);
diff --git a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs
index 73e914083..ff35f86e1 100644
--- a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs
+++ b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs
@@ -16,10 +16,11 @@ namespace Ryujinx.Audio.Backends.OpenAL
private bool _isActive;
private readonly Queue _queuedBuffers;
private ulong _playedSampleCount;
+ private float _volume;
private readonly object _lock = new();
- public OpenALHardwareDeviceSession(OpenALHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
+ public OpenALHardwareDeviceSession(OpenALHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
{
_driver = driver;
_queuedBuffers = new Queue();
@@ -27,7 +28,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
_targetFormat = GetALFormat();
_isActive = false;
_playedSampleCount = 0;
- SetVolume(requestedVolume);
+ SetVolume(1f);
}
private ALFormat GetALFormat()
@@ -85,17 +86,22 @@ namespace Ryujinx.Audio.Backends.OpenAL
public override void SetVolume(float volume)
{
- lock (_lock)
- {
- AL.Source(_sourceId, ALSourcef.Gain, volume);
- }
+ _volume = volume;
+
+ UpdateMasterVolume(_driver.Volume);
}
public override float GetVolume()
{
- AL.GetSource(_sourceId, ALSourcef.Gain, out float volume);
+ return _volume;
+ }
- return volume;
+ public void UpdateMasterVolume(float newVolume)
+ {
+ lock (_lock)
+ {
+ AL.Source(_sourceId, ALSourcef.Gain, newVolume * _volume);
+ }
}
public override void Start()
diff --git a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs
index 58137bb38..03a036936 100644
--- a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs
+++ b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs
@@ -20,6 +20,8 @@ namespace Ryujinx.Audio.Backends.SDL2
private readonly bool _supportSurroundConfiguration;
+ public float Volume { get; set; }
+
// TODO: Add this to SDL2-CS
// NOTE: We use a DllImport here because of marshaling issue for spec.
#pragma warning disable SYSLIB1054
@@ -48,6 +50,8 @@ namespace Ryujinx.Audio.Backends.SDL2
{
_supportSurroundConfiguration = spec.channels >= 6;
}
+
+ Volume = 1f;
}
public static bool IsSupported => IsSupportedInternal();
@@ -74,7 +78,7 @@ namespace Ryujinx.Audio.Backends.SDL2
return _pauseEvent;
}
- public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
+ public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (channelCount == 0)
{
@@ -91,7 +95,7 @@ namespace Ryujinx.Audio.Backends.SDL2
throw new NotImplementedException("Input direction is currently not implemented on SDL2 backend!");
}
- SDL2HardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
+ SDL2HardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
_sessions.TryAdd(session, 0);
diff --git a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs
index cf3be473e..ea5ae3661 100644
--- a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs
+++ b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs
@@ -1,8 +1,10 @@
using Ryujinx.Audio.Backends.Common;
using Ryujinx.Audio.Common;
using Ryujinx.Common.Logging;
+using Ryujinx.Common.Memory;
using Ryujinx.Memory;
using System;
+using System.Buffers;
using System.Collections.Concurrent;
using System.Threading;
@@ -26,7 +28,7 @@ namespace Ryujinx.Audio.Backends.SDL2
private float _volume;
private readonly ushort _nativeSampleFormat;
- public SDL2HardwareDeviceSession(SDL2HardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
+ public SDL2HardwareDeviceSession(SDL2HardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
{
_driver = driver;
_updateRequiredEvent = _driver.GetUpdateRequiredEvent();
@@ -37,7 +39,7 @@ namespace Ryujinx.Audio.Backends.SDL2
_nativeSampleFormat = SDL2HardwareDeviceDriver.GetSDL2Format(RequestedSampleFormat);
_sampleCount = uint.MaxValue;
_started = false;
- _volume = requestedVolume;
+ _volume = 1f;
}
private void EnsureAudioStreamSetup(AudioBuffer buffer)
@@ -87,7 +89,9 @@ namespace Ryujinx.Audio.Backends.SDL2
return;
}
- byte[] samples = new byte[frameCount * _bytesPerFrame];
+ using SpanOwner samplesOwner = SpanOwner.Rent(frameCount * _bytesPerFrame);
+
+ Span samples = samplesOwner.Span;
_ringBuffer.Read(samples, 0, samples.Length);
@@ -99,7 +103,7 @@ namespace Ryujinx.Audio.Backends.SDL2
streamSpan.Clear();
// Apply volume to written data
- SDL_MixAudioFormat(stream, pStreamSrc, _nativeSampleFormat, (uint)samples.Length, (int)(_volume * SDL_MIX_MAXVOLUME));
+ SDL_MixAudioFormat(stream, pStreamSrc, _nativeSampleFormat, (uint)samples.Length, (int)(_driver.Volume * _volume * SDL_MIX_MAXVOLUME));
}
ulong sampleCount = GetSampleCount(samples.Length);
diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj b/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj
index 1d92d9d2e..5c9423463 100644
--- a/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj
+++ b/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj
@@ -11,15 +11,15 @@
-
+
PreserveNewest
libsoundio.dll
-
+
PreserveNewest
libsoundio.dylib
-
+
PreserveNewest
libsoundio.so
diff --git a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs
index ff0392882..e3e5d2913 100644
--- a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs
+++ b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs
@@ -19,6 +19,25 @@ namespace Ryujinx.Audio.Backends.SoundIo
private readonly ConcurrentDictionary _sessions;
private int _disposeState;
+ private float _volume = 1f;
+
+ public float Volume
+ {
+ get
+ {
+ return _volume;
+ }
+ set
+ {
+ _volume = value;
+
+ foreach (SoundIoHardwareDeviceSession session in _sessions.Keys)
+ {
+ session.UpdateMasterVolume(value);
+ }
+ }
+ }
+
public SoundIoHardwareDeviceDriver()
{
_audioContext = SoundIoContext.Create();
@@ -122,7 +141,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
return _pauseEvent;
}
- public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
+ public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (channelCount == 0)
{
@@ -134,14 +153,12 @@ namespace Ryujinx.Audio.Backends.SoundIo
sampleRate = Constants.TargetSampleRate;
}
- volume = Math.Clamp(volume, 0, 1);
-
if (direction != Direction.Output)
{
throw new NotImplementedException("Input direction is currently not implemented on SoundIO backend!");
}
- SoundIoHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
+ SoundIoHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
_sessions.TryAdd(session, 0);
diff --git a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs
index b9070dc48..2ef83b34b 100644
--- a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs
+++ b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs
@@ -1,8 +1,10 @@
using Ryujinx.Audio.Backends.Common;
using Ryujinx.Audio.Backends.SoundIo.Native;
using Ryujinx.Audio.Common;
+using Ryujinx.Common.Memory;
using Ryujinx.Memory;
using System;
+using System.Buffers;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Threading;
@@ -18,16 +20,18 @@ namespace Ryujinx.Audio.Backends.SoundIo
private readonly DynamicRingBuffer _ringBuffer;
private ulong _playedSampleCount;
private readonly ManualResetEvent _updateRequiredEvent;
+ private float _volume;
private int _disposeState;
- public SoundIoHardwareDeviceSession(SoundIoHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
+ public SoundIoHardwareDeviceSession(SoundIoHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
{
_driver = driver;
_updateRequiredEvent = _driver.GetUpdateRequiredEvent();
_queuedBuffers = new ConcurrentQueue();
_ringBuffer = new DynamicRingBuffer();
+ _volume = 1f;
- SetupOutputStream(requestedVolume);
+ SetupOutputStream(driver.Volume);
}
private void SetupOutputStream(float requestedVolume)
@@ -35,7 +39,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
_outputStream = _driver.OpenStream(RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount);
_outputStream.WriteCallback += Update;
_outputStream.Volume = requestedVolume;
- // TODO: Setup other callbacks (errors, ect).
+ // TODO: Setup other callbacks (errors, etc.)
_outputStream.Open();
}
@@ -47,7 +51,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
public override float GetVolume()
{
- return _outputStream.Volume;
+ return _volume;
}
public override void PrepareToClose() { }
@@ -63,7 +67,14 @@ namespace Ryujinx.Audio.Backends.SoundIo
public override void SetVolume(float volume)
{
- _outputStream.SetVolume(volume);
+ _volume = volume;
+
+ _outputStream.SetVolume(_driver.Volume * volume);
+ }
+
+ public void UpdateMasterVolume(float newVolume)
+ {
+ _outputStream.SetVolume(newVolume * _volume);
}
public override void Start()
@@ -111,7 +122,9 @@ namespace Ryujinx.Audio.Backends.SoundIo
int channelCount = areas.Length;
- byte[] samples = new byte[frameCount * bytesPerFrame];
+ using SpanOwner samplesOwner = SpanOwner.Rent(frameCount * bytesPerFrame);
+
+ Span samples = samplesOwner.Span;
_ringBuffer.Read(samples, 0, samples.Length);
diff --git a/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs b/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs
index 05dd2162a..7aefe8865 100644
--- a/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs
+++ b/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs
@@ -1,5 +1,7 @@
using Ryujinx.Common;
+using Ryujinx.Common.Memory;
using System;
+using System.Buffers;
namespace Ryujinx.Audio.Backends.Common
{
@@ -12,7 +14,8 @@ namespace Ryujinx.Audio.Backends.Common
private readonly object _lock = new();
- private byte[] _buffer;
+ private MemoryOwner _bufferOwner;
+ private Memory _buffer;
private int _size;
private int _headOffset;
private int _tailOffset;
@@ -21,7 +24,8 @@ namespace Ryujinx.Audio.Backends.Common
public DynamicRingBuffer(int initialCapacity = RingBufferAlignment)
{
- _buffer = new byte[initialCapacity];
+ _bufferOwner = MemoryOwner.RentCleared(initialCapacity);
+ _buffer = _bufferOwner.Memory;
}
public void Clear()
@@ -33,6 +37,11 @@ namespace Ryujinx.Audio.Backends.Common
public void Clear(int size)
{
+ if (size == 0)
+ {
+ return;
+ }
+
lock (_lock)
{
if (size > _size)
@@ -40,11 +49,6 @@ namespace Ryujinx.Audio.Backends.Common
size = _size;
}
- if (size == 0)
- {
- return;
- }
-
_headOffset = (_headOffset + size) % _buffer.Length;
_size -= size;
@@ -58,28 +62,31 @@ namespace Ryujinx.Audio.Backends.Common
private void SetCapacityLocked(int capacity)
{
- byte[] buffer = new byte[capacity];
+ MemoryOwner newBufferOwner = MemoryOwner.RentCleared(capacity);
+ Memory newBuffer = newBufferOwner.Memory;
if (_size > 0)
{
if (_headOffset < _tailOffset)
{
- Buffer.BlockCopy(_buffer, _headOffset, buffer, 0, _size);
+ _buffer.Slice(_headOffset, _size).CopyTo(newBuffer);
}
else
{
- Buffer.BlockCopy(_buffer, _headOffset, buffer, 0, _buffer.Length - _headOffset);
- Buffer.BlockCopy(_buffer, 0, buffer, _buffer.Length - _headOffset, _tailOffset);
+ _buffer[_headOffset..].CopyTo(newBuffer);
+ _buffer[.._tailOffset].CopyTo(newBuffer[(_buffer.Length - _headOffset)..]);
}
}
- _buffer = buffer;
+ _bufferOwner.Dispose();
+
+ _bufferOwner = newBufferOwner;
+ _buffer = newBuffer;
_headOffset = 0;
_tailOffset = _size;
}
-
- public void Write(T[] buffer, int index, int count)
+ public void Write(ReadOnlySpan buffer, int index, int count)
{
if (count == 0)
{
@@ -99,17 +106,17 @@ namespace Ryujinx.Audio.Backends.Common
if (tailLength >= count)
{
- Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, count);
+ buffer.Slice(index, count).CopyTo(_buffer.Span[_tailOffset..]);
}
else
{
- Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, tailLength);
- Buffer.BlockCopy(buffer, index + tailLength, _buffer, 0, count - tailLength);
+ buffer.Slice(index, tailLength).CopyTo(_buffer.Span[_tailOffset..]);
+ buffer.Slice(index + tailLength, count - tailLength).CopyTo(_buffer.Span);
}
}
else
{
- Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, count);
+ buffer.Slice(index, count).CopyTo(_buffer.Span[_tailOffset..]);
}
_size += count;
@@ -117,8 +124,13 @@ namespace Ryujinx.Audio.Backends.Common
}
}
- public int Read(T[] buffer, int index, int count)
+ public int Read(Span buffer, int index, int count)
{
+ if (count == 0)
+ {
+ return 0;
+ }
+
lock (_lock)
{
if (count > _size)
@@ -126,14 +138,9 @@ namespace Ryujinx.Audio.Backends.Common
count = _size;
}
- if (count == 0)
- {
- return 0;
- }
-
if (_headOffset < _tailOffset)
{
- Buffer.BlockCopy(_buffer, _headOffset, buffer, index, count);
+ _buffer.Span.Slice(_headOffset, count).CopyTo(buffer[index..]);
}
else
{
@@ -141,12 +148,12 @@ namespace Ryujinx.Audio.Backends.Common
if (tailLength >= count)
{
- Buffer.BlockCopy(_buffer, _headOffset, buffer, index, count);
+ _buffer.Span.Slice(_headOffset, count).CopyTo(buffer[index..]);
}
else
{
- Buffer.BlockCopy(_buffer, _headOffset, buffer, index, tailLength);
- Buffer.BlockCopy(_buffer, 0, buffer, index + tailLength, count - tailLength);
+ _buffer.Span.Slice(_headOffset, tailLength).CopyTo(buffer[index..]);
+ _buffer.Span[..(count - tailLength)].CopyTo(buffer[(index + tailLength)..]);
}
}
diff --git a/src/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs b/src/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs
index 3f3806c3e..a2c2cdcd0 100644
--- a/src/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs
+++ b/src/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs
@@ -16,6 +16,12 @@ namespace Ryujinx.Audio.Backends.CompatLayer
public static bool IsSupported => true;
+ public float Volume
+ {
+ get => _realDriver.Volume;
+ set => _realDriver.Volume = value;
+ }
+
public CompatLayerHardwareDeviceDriver(IHardwareDeviceDriver realDevice)
{
_realDriver = realDevice;
@@ -90,7 +96,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
throw new ArgumentException("No valid sample format configuration found!");
}
- public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
+ public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (channelCount == 0)
{
@@ -102,8 +108,6 @@ namespace Ryujinx.Audio.Backends.CompatLayer
sampleRate = Constants.TargetSampleRate;
}
- volume = Math.Clamp(volume, 0, 1);
-
if (!_realDriver.SupportsDirection(direction))
{
if (direction == Direction.Input)
@@ -119,7 +123,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
SampleFormat hardwareSampleFormat = SelectHardwareSampleFormat(sampleFormat);
uint hardwareChannelCount = SelectHardwareChannelCount(channelCount);
- IHardwareDeviceSession realSession = _realDriver.OpenDeviceSession(direction, memoryManager, hardwareSampleFormat, sampleRate, hardwareChannelCount, volume);
+ IHardwareDeviceSession realSession = _realDriver.OpenDeviceSession(direction, memoryManager, hardwareSampleFormat, sampleRate, hardwareChannelCount);
if (hardwareChannelCount == channelCount && hardwareSampleFormat == sampleFormat)
{
diff --git a/src/Ryujinx.Audio/Backends/CompatLayer/Downmixing.cs b/src/Ryujinx.Audio/Backends/CompatLayer/Downmixing.cs
index ffd427a5e..7a5ea0deb 100644
--- a/src/Ryujinx.Audio/Backends/CompatLayer/Downmixing.cs
+++ b/src/Ryujinx.Audio/Backends/CompatLayer/Downmixing.cs
@@ -31,7 +31,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
private const int Minus6dBInQ15 = (int)(0.501f * RawQ15One);
private const int Minus12dBInQ15 = (int)(0.251f * RawQ15One);
- private static readonly int[] _defaultSurroundToStereoCoefficients = new int[4]
+ private static readonly long[] _defaultSurroundToStereoCoefficients = new long[4]
{
RawQ15One,
Minus3dBInQ15,
@@ -39,7 +39,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
Minus3dBInQ15,
};
- private static readonly int[] _defaultStereoToMonoCoefficients = new int[2]
+ private static readonly long[] _defaultStereoToMonoCoefficients = new long[2]
{
Minus6dBInQ15,
Minus6dBInQ15,
@@ -62,19 +62,23 @@ namespace Ryujinx.Audio.Backends.CompatLayer
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static short DownMixStereoToMono(ReadOnlySpan coefficients, short left, short right)
+ private static short DownMixStereoToMono(ReadOnlySpan coefficients, short left, short right)
{
- return (short)((left * coefficients[0] + right * coefficients[1]) >> Q15Bits);
+ return (short)Math.Clamp((left * coefficients[0] + right * coefficients[1]) >> Q15Bits, short.MinValue, short.MaxValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static short DownMixSurroundToStereo(ReadOnlySpan coefficients, short back, short lfe, short center, short front)
+ private static short DownMixSurroundToStereo(ReadOnlySpan coefficients, short back, short lfe, short center, short front)
{
- return (short)((coefficients[3] * back + coefficients[2] * lfe + coefficients[1] * center + coefficients[0] * front + RawQ15HalfOne) >> Q15Bits);
+ return (short)Math.Clamp(
+ (coefficients[3] * back +
+ coefficients[2] * lfe +
+ coefficients[1] * center +
+ coefficients[0] * front + RawQ15HalfOne) >> Q15Bits, short.MinValue, short.MaxValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static short[] DownMixSurroundToStereo(ReadOnlySpan coefficients, ReadOnlySpan data)
+ private static short[] DownMixSurroundToStereo(ReadOnlySpan coefficients, ReadOnlySpan data)
{
int samplePerChannelCount = data.Length / SurroundChannelCount;
@@ -94,7 +98,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static short[] DownMixStereoToMono(ReadOnlySpan coefficients, ReadOnlySpan data)
+ private static short[] DownMixStereoToMono(ReadOnlySpan coefficients, ReadOnlySpan data)
{
int samplePerChannelCount = data.Length / StereoChannelCount;
diff --git a/src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceDriver.cs b/src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceDriver.cs
index cdd5eb8a8..b46b38f5a 100644
--- a/src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceDriver.cs
+++ b/src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceDriver.cs
@@ -16,15 +16,17 @@ namespace Ryujinx.Audio.Backends.DelayLayer
public ulong SampleDelay48k;
+ public float Volume { get; set; }
+
public DelayLayerHardwareDeviceDriver(IHardwareDeviceDriver realDevice, ulong sampleDelay48k)
{
_realDriver = realDevice;
SampleDelay48k = sampleDelay48k;
}
- public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
+ public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
- IHardwareDeviceSession session = _realDriver.OpenDeviceSession(direction, memoryManager, sampleFormat, sampleRate, channelCount, volume);
+ IHardwareDeviceSession session = _realDriver.OpenDeviceSession(direction, memoryManager, sampleFormat, sampleRate, channelCount);
if (direction == Direction.Output)
{
diff --git a/src/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs b/src/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs
index bac21c448..3a3c1d1b1 100644
--- a/src/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs
+++ b/src/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs
@@ -14,13 +14,17 @@ namespace Ryujinx.Audio.Backends.Dummy
public static bool IsSupported => true;
+ public float Volume { get; set; }
+
public DummyHardwareDeviceDriver()
{
_updateRequiredEvent = new ManualResetEvent(false);
_pauseEvent = new ManualResetEvent(true);
+
+ Volume = 1f;
}
- public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
+ public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (sampleRate == 0)
{
@@ -34,7 +38,7 @@ namespace Ryujinx.Audio.Backends.Dummy
if (direction == Direction.Output)
{
- return new DummyHardwareDeviceSessionOutput(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
+ return new DummyHardwareDeviceSessionOutput(this, memoryManager, sampleFormat, sampleRate, channelCount);
}
return new DummyHardwareDeviceSessionInput(this, memoryManager);
diff --git a/src/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceSessionOutput.cs b/src/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceSessionOutput.cs
index 1c248faaa..34cf653c2 100644
--- a/src/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceSessionOutput.cs
+++ b/src/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceSessionOutput.cs
@@ -13,9 +13,9 @@ namespace Ryujinx.Audio.Backends.Dummy
private ulong _playedSampleCount;
- public DummyHardwareDeviceSessionOutput(IHardwareDeviceDriver manager, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
+ public DummyHardwareDeviceSessionOutput(IHardwareDeviceDriver manager, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
{
- _volume = requestedVolume;
+ _volume = 1f;
_manager = manager;
}
diff --git a/src/Ryujinx.Audio/Input/AudioInputManager.cs b/src/Ryujinx.Audio/Input/AudioInputManager.cs
index 4d1796c96..d56997e9c 100644
--- a/src/Ryujinx.Audio/Input/AudioInputManager.cs
+++ b/src/Ryujinx.Audio/Input/AudioInputManager.cs
@@ -166,7 +166,6 @@ namespace Ryujinx.Audio.Input
///
/// If true, filter disconnected devices
/// The list of all audio inputs name
-#pragma warning disable CA1822 // Mark member as static
public string[] ListAudioIns(bool filtered)
{
if (filtered)
@@ -176,7 +175,6 @@ namespace Ryujinx.Audio.Input
return new[] { Constants.DefaultDeviceInputName };
}
-#pragma warning restore CA1822
///
/// Open a new .
@@ -188,8 +186,6 @@ namespace Ryujinx.Audio.Input
/// The input device name wanted by the user
/// The sample format to use
/// The user configuration
- /// The applet resource user id of the application
- /// The process handle of the application
/// A reporting an error or a success
public ResultCode OpenAudioIn(out string outputDeviceName,
out AudioOutputConfiguration outputConfiguration,
@@ -197,9 +193,7 @@ namespace Ryujinx.Audio.Input
IVirtualMemoryManager memoryManager,
string inputDeviceName,
SampleFormat sampleFormat,
- ref AudioInputConfiguration parameter,
- ulong appletResourceUserId,
- uint processHandle)
+ ref AudioInputConfiguration parameter)
{
int sessionId = AcquireSessionId();
diff --git a/src/Ryujinx.Audio/Integration/HardwareDeviceImpl.cs b/src/Ryujinx.Audio/Integration/HardwareDeviceImpl.cs
index 576954b96..1369f953a 100644
--- a/src/Ryujinx.Audio/Integration/HardwareDeviceImpl.cs
+++ b/src/Ryujinx.Audio/Integration/HardwareDeviceImpl.cs
@@ -13,9 +13,9 @@ namespace Ryujinx.Audio.Integration
private readonly byte[] _buffer;
- public HardwareDeviceImpl(IHardwareDeviceDriver deviceDriver, uint channelCount, uint sampleRate, float volume)
+ public HardwareDeviceImpl(IHardwareDeviceDriver deviceDriver, uint channelCount, uint sampleRate)
{
- _session = deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, null, SampleFormat.PcmInt16, sampleRate, channelCount, volume);
+ _session = deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, null, SampleFormat.PcmInt16, sampleRate, channelCount);
_channelCount = channelCount;
_sampleRate = sampleRate;
_currentBufferTag = 0;
diff --git a/src/Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs b/src/Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs
index 9c812fb9a..95b0e4e5e 100644
--- a/src/Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs
+++ b/src/Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs
@@ -16,7 +16,9 @@ namespace Ryujinx.Audio.Integration
Output,
}
- IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume = 1f);
+ float Volume { get; set; }
+
+ IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount);
ManualResetEvent GetUpdateRequiredEvent();
ManualResetEvent GetPauseEvent();
diff --git a/src/Ryujinx.Audio/Output/AudioOutputManager.cs b/src/Ryujinx.Audio/Output/AudioOutputManager.cs
index 5232357bb..308cd1564 100644
--- a/src/Ryujinx.Audio/Output/AudioOutputManager.cs
+++ b/src/Ryujinx.Audio/Output/AudioOutputManager.cs
@@ -165,12 +165,10 @@ namespace Ryujinx.Audio.Output
/// Get the list of all audio outputs name.
///
/// The list of all audio outputs name
-#pragma warning disable CA1822 // Mark member as static
public string[] ListAudioOuts()
{
return new[] { Constants.DefaultDeviceOutputName };
}
-#pragma warning restore CA1822
///
/// Open a new .
@@ -182,9 +180,6 @@ namespace Ryujinx.Audio.Output
/// The input device name wanted by the user
/// The sample format to use
/// The user configuration
- /// The applet resource user id of the application
- /// The process handle of the application
- /// The volume level to request
/// A reporting an error or a success
public ResultCode OpenAudioOut(out string outputDeviceName,
out AudioOutputConfiguration outputConfiguration,
@@ -192,16 +187,13 @@ namespace Ryujinx.Audio.Output
IVirtualMemoryManager memoryManager,
string inputDeviceName,
SampleFormat sampleFormat,
- ref AudioInputConfiguration parameter,
- ulong appletResourceUserId,
- uint processHandle,
- float volume)
+ ref AudioInputConfiguration parameter)
{
int sessionId = AcquireSessionId();
_sessionsBufferEvents[sessionId].Clear();
- IHardwareDeviceSession deviceSession = _deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, memoryManager, sampleFormat, parameter.SampleRate, parameter.ChannelCount, volume);
+ IHardwareDeviceSession deviceSession = _deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, memoryManager, sampleFormat, parameter.SampleRate, parameter.ChannelCount);
AudioOutputSystem audioOut = new(this, _lock, deviceSession, _sessionsBufferEvents[sessionId]);
@@ -234,41 +226,6 @@ namespace Ryujinx.Audio.Output
return result;
}
- ///
- /// Sets the volume for all output devices.
- ///
- /// The volume to set.
- public void SetVolume(float volume)
- {
- if (_sessions != null)
- {
- foreach (AudioOutputSystem session in _sessions)
- {
- session?.SetVolume(volume);
- }
- }
- }
-
- ///
- /// Gets the volume for all output devices.
- ///
- /// A float indicating the volume level.
- public float GetVolume()
- {
- if (_sessions != null)
- {
- foreach (AudioOutputSystem session in _sessions)
- {
- if (session != null)
- {
- return session.GetVolume();
- }
- }
- }
-
- return 0.0f;
- }
-
public void Dispose()
{
GC.SuppressFinalize(this);
diff --git a/src/Ryujinx.Audio/Renderer/Common/BehaviourParameter.cs b/src/Ryujinx.Audio/Renderer/Common/BehaviourParameter.cs
index b0963c935..3b8d15dc5 100644
--- a/src/Ryujinx.Audio/Renderer/Common/BehaviourParameter.cs
+++ b/src/Ryujinx.Audio/Renderer/Common/BehaviourParameter.cs
@@ -25,7 +25,7 @@ namespace Ryujinx.Audio.Renderer.Common
public ulong Flags;
///
- /// Represents an error during .
+ /// Represents an error during .
///
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ErrorInfo
diff --git a/src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs b/src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs
index 7efe3b02b..98b224ebf 100644
--- a/src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs
+++ b/src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs
@@ -4,7 +4,7 @@ using System.Runtime.CompilerServices;
namespace Ryujinx.Audio.Renderer.Common
{
///
- /// Update data header used for input and output of .
+ /// Update data header used for input and output of .
///
public struct UpdateDataHeader
{
diff --git a/src/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs b/src/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs
index 608381af1..7f881373f 100644
--- a/src/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs
+++ b/src/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs
@@ -15,7 +15,6 @@ namespace Ryujinx.Audio.Renderer.Common
{
public const int Align = 0x10;
public const int BiquadStateOffset = 0x0;
- public const int BiquadStateSize = 0x10;
///
/// The state of the biquad filters of this voice.
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/AudioProcessor.cs b/src/Ryujinx.Audio/Renderer/Dsp/AudioProcessor.cs
index 9c885b2cf..3e11df056 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/AudioProcessor.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/AudioProcessor.cs
@@ -45,7 +45,6 @@ namespace Ryujinx.Audio.Renderer.Dsp
_event = new ManualResetEvent(false);
}
-#pragma warning disable IDE0051 // Remove unused private member
private static uint GetHardwareChannelCount(IHardwareDeviceDriver deviceDriver)
{
// Get the real device driver (In case the compat layer is on top of it).
@@ -59,9 +58,8 @@ namespace Ryujinx.Audio.Renderer.Dsp
// NOTE: We default to stereo as this will get downmixed to mono by the compat layer if it's not compatible.
return 2;
}
-#pragma warning restore IDE0051
- public void Start(IHardwareDeviceDriver deviceDriver, float volume)
+ public void Start(IHardwareDeviceDriver deviceDriver)
{
OutputDevices = new IHardwareDevice[Constants.AudioRendererSessionCountMax];
@@ -70,7 +68,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
for (int i = 0; i < OutputDevices.Length; i++)
{
// TODO: Don't hardcode sample rate.
- OutputDevices[i] = new HardwareDeviceImpl(deviceDriver, channelCount, Constants.TargetSampleRate, volume);
+ OutputDevices[i] = new HardwareDeviceImpl(deviceDriver, channelCount, Constants.TargetSampleRate);
}
_mailbox = new Mailbox();
@@ -231,33 +229,6 @@ namespace Ryujinx.Audio.Renderer.Dsp
_mailbox.SendResponse(MailboxMessage.Stop);
}
- public float GetVolume()
- {
- if (OutputDevices != null)
- {
- foreach (IHardwareDevice outputDevice in OutputDevices)
- {
- if (outputDevice != null)
- {
- return outputDevice.GetVolume();
- }
- }
- }
-
- return 0f;
- }
-
- public void SetVolume(float volume)
- {
- if (OutputDevices != null)
- {
- foreach (IHardwareDevice outputDevice in OutputDevices)
- {
- outputDevice?.SetVolume(volume);
- }
- }
- }
-
public void Dispose()
{
GC.SuppressFinalize(this);
@@ -269,6 +240,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
if (disposing)
{
_event.Dispose();
+ _mailbox?.Dispose();
}
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/BiquadFilterHelper.cs b/src/Ryujinx.Audio/Renderer/Dsp/BiquadFilterHelper.cs
index 1a51a1fbd..31f614d67 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/BiquadFilterHelper.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/BiquadFilterHelper.cs
@@ -16,10 +16,15 @@ namespace Ryujinx.Audio.Renderer.Dsp
/// The biquad filter parameter
/// The biquad filter state
/// The output buffer to write the result
- /// The input buffer to write the result
+ /// The input buffer to read the samples from
/// The count of samples to process
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void ProcessBiquadFilter(ref BiquadFilterParameter parameter, ref BiquadFilterState state, Span outputBuffer, ReadOnlySpan inputBuffer, uint sampleCount)
+ public static void ProcessBiquadFilter(
+ ref BiquadFilterParameter parameter,
+ ref BiquadFilterState state,
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ uint sampleCount)
{
float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter);
float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter);
@@ -40,6 +45,96 @@ namespace Ryujinx.Audio.Renderer.Dsp
}
}
+ ///
+ /// Apply a single biquad filter and mix the result into the output buffer.
+ ///
+ /// This is implemented with a direct form 1.
+ /// The biquad filter parameter
+ /// The biquad filter state
+ /// The output buffer to write the result
+ /// The input buffer to read the samples from
+ /// The count of samples to process
+ /// Mix volume
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ProcessBiquadFilterAndMix(
+ ref BiquadFilterParameter parameter,
+ ref BiquadFilterState state,
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ uint sampleCount,
+ float volume)
+ {
+ float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter);
+ float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter);
+ float a2 = FixedPointHelper.ToFloat(parameter.Numerator[2], FixedPointPrecisionForParameter);
+
+ float b1 = FixedPointHelper.ToFloat(parameter.Denominator[0], FixedPointPrecisionForParameter);
+ float b2 = FixedPointHelper.ToFloat(parameter.Denominator[1], FixedPointPrecisionForParameter);
+
+ for (int i = 0; i < sampleCount; i++)
+ {
+ float input = inputBuffer[i];
+ float output = input * a0 + state.State0 * a1 + state.State1 * a2 + state.State2 * b1 + state.State3 * b2;
+
+ state.State1 = state.State0;
+ state.State0 = input;
+ state.State3 = state.State2;
+ state.State2 = output;
+
+ outputBuffer[i] += FloatingPointHelper.MultiplyRoundUp(output, volume);
+ }
+ }
+
+ ///
+ /// Apply a single biquad filter and mix the result into the output buffer with volume ramp.
+ ///
+ /// This is implemented with a direct form 1.
+ /// The biquad filter parameter
+ /// The biquad filter state
+ /// The output buffer to write the result
+ /// The input buffer to read the samples from
+ /// The count of samples to process
+ /// Initial mix volume
+ /// Volume increment step
+ /// Last filtered sample value
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float ProcessBiquadFilterAndMixRamp(
+ ref BiquadFilterParameter parameter,
+ ref BiquadFilterState state,
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ uint sampleCount,
+ float volume,
+ float ramp)
+ {
+ float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter);
+ float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter);
+ float a2 = FixedPointHelper.ToFloat(parameter.Numerator[2], FixedPointPrecisionForParameter);
+
+ float b1 = FixedPointHelper.ToFloat(parameter.Denominator[0], FixedPointPrecisionForParameter);
+ float b2 = FixedPointHelper.ToFloat(parameter.Denominator[1], FixedPointPrecisionForParameter);
+
+ float mixState = 0f;
+
+ for (int i = 0; i < sampleCount; i++)
+ {
+ float input = inputBuffer[i];
+ float output = input * a0 + state.State0 * a1 + state.State1 * a2 + state.State2 * b1 + state.State3 * b2;
+
+ state.State1 = state.State0;
+ state.State0 = input;
+ state.State3 = state.State2;
+ state.State2 = output;
+
+ mixState = FloatingPointHelper.MultiplyRoundUp(output, volume);
+
+ outputBuffer[i] += mixState;
+ volume += ramp;
+ }
+
+ return mixState;
+ }
+
///
/// Apply multiple biquad filter.
///
@@ -47,10 +142,15 @@ namespace Ryujinx.Audio.Renderer.Dsp
/// The biquad filter parameter
/// The biquad filter state
/// The output buffer to write the result
- /// The input buffer to write the result
+ /// The input buffer to read the samples from
/// The count of samples to process
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void ProcessBiquadFilter(ReadOnlySpan parameters, Span states, Span outputBuffer, ReadOnlySpan inputBuffer, uint sampleCount)
+ public static void ProcessBiquadFilter(
+ ReadOnlySpan parameters,
+ Span states,
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ uint sampleCount)
{
for (int stageIndex = 0; stageIndex < parameters.Length; stageIndex++)
{
@@ -67,7 +167,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
for (int i = 0; i < sampleCount; i++)
{
- float input = inputBuffer[i];
+ float input = stageIndex != 0 ? outputBuffer[i] : inputBuffer[i];
float output = input * a0 + state.State0 * a1 + state.State1 * a2 + state.State2 * b1 + state.State3 * b2;
state.State1 = state.State0;
@@ -79,5 +179,129 @@ namespace Ryujinx.Audio.Renderer.Dsp
}
}
}
+
+ ///
+ /// Apply double biquad filter and mix the result into the output buffer.
+ ///
+ /// This is implemented with a direct form 1.
+ /// The biquad filter parameter
+ /// The biquad filter state
+ /// The output buffer to write the result
+ /// The input buffer to read the samples from
+ /// The count of samples to process
+ /// Mix volume
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ProcessDoubleBiquadFilterAndMix(
+ ref BiquadFilterParameter parameter0,
+ ref BiquadFilterParameter parameter1,
+ ref BiquadFilterState state0,
+ ref BiquadFilterState state1,
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ uint sampleCount,
+ float volume)
+ {
+ float a00 = FixedPointHelper.ToFloat(parameter0.Numerator[0], FixedPointPrecisionForParameter);
+ float a10 = FixedPointHelper.ToFloat(parameter0.Numerator[1], FixedPointPrecisionForParameter);
+ float a20 = FixedPointHelper.ToFloat(parameter0.Numerator[2], FixedPointPrecisionForParameter);
+
+ float b10 = FixedPointHelper.ToFloat(parameter0.Denominator[0], FixedPointPrecisionForParameter);
+ float b20 = FixedPointHelper.ToFloat(parameter0.Denominator[1], FixedPointPrecisionForParameter);
+
+ float a01 = FixedPointHelper.ToFloat(parameter1.Numerator[0], FixedPointPrecisionForParameter);
+ float a11 = FixedPointHelper.ToFloat(parameter1.Numerator[1], FixedPointPrecisionForParameter);
+ float a21 = FixedPointHelper.ToFloat(parameter1.Numerator[2], FixedPointPrecisionForParameter);
+
+ float b11 = FixedPointHelper.ToFloat(parameter1.Denominator[0], FixedPointPrecisionForParameter);
+ float b21 = FixedPointHelper.ToFloat(parameter1.Denominator[1], FixedPointPrecisionForParameter);
+
+ for (int i = 0; i < sampleCount; i++)
+ {
+ float input = inputBuffer[i];
+ float output = input * a00 + state0.State0 * a10 + state0.State1 * a20 + state0.State2 * b10 + state0.State3 * b20;
+
+ state0.State1 = state0.State0;
+ state0.State0 = input;
+ state0.State3 = state0.State2;
+ state0.State2 = output;
+
+ input = output;
+ output = input * a01 + state1.State0 * a11 + state1.State1 * a21 + state1.State2 * b11 + state1.State3 * b21;
+
+ state1.State1 = state1.State0;
+ state1.State0 = input;
+ state1.State3 = state1.State2;
+ state1.State2 = output;
+
+ outputBuffer[i] += FloatingPointHelper.MultiplyRoundUp(output, volume);
+ }
+ }
+
+ ///
+ /// Apply double biquad filter and mix the result into the output buffer with volume ramp.
+ ///
+ /// This is implemented with a direct form 1.
+ /// The biquad filter parameter
+ /// The biquad filter state
+ /// The output buffer to write the result
+ /// The input buffer to read the samples from
+ /// The count of samples to process
+ /// Initial mix volume
+ /// Volume increment step
+ /// Last filtered sample value
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float ProcessDoubleBiquadFilterAndMixRamp(
+ ref BiquadFilterParameter parameter0,
+ ref BiquadFilterParameter parameter1,
+ ref BiquadFilterState state0,
+ ref BiquadFilterState state1,
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ uint sampleCount,
+ float volume,
+ float ramp)
+ {
+ float a00 = FixedPointHelper.ToFloat(parameter0.Numerator[0], FixedPointPrecisionForParameter);
+ float a10 = FixedPointHelper.ToFloat(parameter0.Numerator[1], FixedPointPrecisionForParameter);
+ float a20 = FixedPointHelper.ToFloat(parameter0.Numerator[2], FixedPointPrecisionForParameter);
+
+ float b10 = FixedPointHelper.ToFloat(parameter0.Denominator[0], FixedPointPrecisionForParameter);
+ float b20 = FixedPointHelper.ToFloat(parameter0.Denominator[1], FixedPointPrecisionForParameter);
+
+ float a01 = FixedPointHelper.ToFloat(parameter1.Numerator[0], FixedPointPrecisionForParameter);
+ float a11 = FixedPointHelper.ToFloat(parameter1.Numerator[1], FixedPointPrecisionForParameter);
+ float a21 = FixedPointHelper.ToFloat(parameter1.Numerator[2], FixedPointPrecisionForParameter);
+
+ float b11 = FixedPointHelper.ToFloat(parameter1.Denominator[0], FixedPointPrecisionForParameter);
+ float b21 = FixedPointHelper.ToFloat(parameter1.Denominator[1], FixedPointPrecisionForParameter);
+
+ float mixState = 0f;
+
+ for (int i = 0; i < sampleCount; i++)
+ {
+ float input = inputBuffer[i];
+ float output = input * a00 + state0.State0 * a10 + state0.State1 * a20 + state0.State2 * b10 + state0.State3 * b20;
+
+ state0.State1 = state0.State0;
+ state0.State0 = input;
+ state0.State3 = state0.State2;
+ state0.State2 = output;
+
+ input = output;
+ output = input * a01 + state1.State0 * a11 + state1.State1 * a21 + state1.State2 * b11 + state1.State3 * b21;
+
+ state1.State1 = state1.State0;
+ state1.State0 = input;
+ state1.State3 = state1.State2;
+ state1.State2 = output;
+
+ mixState = FloatingPointHelper.MultiplyRoundUp(output, volume);
+
+ outputBuffer[i] += mixState;
+ volume += ramp;
+ }
+
+ return mixState;
+ }
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterAndMixCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterAndMixCommand.cs
new file mode 100644
index 000000000..106fc0357
--- /dev/null
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterAndMixCommand.cs
@@ -0,0 +1,123 @@
+using Ryujinx.Audio.Renderer.Common;
+using Ryujinx.Audio.Renderer.Dsp.State;
+using Ryujinx.Audio.Renderer.Parameter;
+using System;
+
+namespace Ryujinx.Audio.Renderer.Dsp.Command
+{
+ public class BiquadFilterAndMixCommand : ICommand
+ {
+ public bool Enabled { get; set; }
+
+ public int NodeId { get; }
+
+ public CommandType CommandType => CommandType.BiquadFilterAndMix;
+
+ public uint EstimatedProcessingTime { get; set; }
+
+ public ushort InputBufferIndex { get; }
+ public ushort OutputBufferIndex { get; }
+
+ private BiquadFilterParameter _parameter;
+
+ public Memory BiquadFilterState { get; }
+ public Memory PreviousBiquadFilterState { get; }
+
+ public Memory State { get; }
+
+ public int LastSampleIndex { get; }
+
+ public float Volume0 { get; }
+ public float Volume1 { get; }
+
+ public bool NeedInitialization { get; }
+ public bool HasVolumeRamp { get; }
+ public bool IsFirstMixBuffer { get; }
+
+ public BiquadFilterAndMixCommand(
+ float volume0,
+ float volume1,
+ uint inputBufferIndex,
+ uint outputBufferIndex,
+ int lastSampleIndex,
+ Memory state,
+ ref BiquadFilterParameter filter,
+ Memory biquadFilterState,
+ Memory previousBiquadFilterState,
+ bool needInitialization,
+ bool hasVolumeRamp,
+ bool isFirstMixBuffer,
+ int nodeId)
+ {
+ Enabled = true;
+ NodeId = nodeId;
+
+ InputBufferIndex = (ushort)inputBufferIndex;
+ OutputBufferIndex = (ushort)outputBufferIndex;
+
+ _parameter = filter;
+ BiquadFilterState = biquadFilterState;
+ PreviousBiquadFilterState = previousBiquadFilterState;
+
+ State = state;
+ LastSampleIndex = lastSampleIndex;
+
+ Volume0 = volume0;
+ Volume1 = volume1;
+
+ NeedInitialization = needInitialization;
+ HasVolumeRamp = hasVolumeRamp;
+ IsFirstMixBuffer = isFirstMixBuffer;
+ }
+
+ public void Process(CommandList context)
+ {
+ ReadOnlySpan inputBuffer = context.GetBuffer(InputBufferIndex);
+ Span outputBuffer = context.GetBuffer(OutputBufferIndex);
+
+ if (NeedInitialization)
+ {
+ // If there is no previous state, initialize to zero.
+
+ BiquadFilterState.Span[0] = new BiquadFilterState();
+ }
+ else if (IsFirstMixBuffer)
+ {
+ // This is the first buffer, set previous state to current state.
+
+ PreviousBiquadFilterState.Span[0] = BiquadFilterState.Span[0];
+ }
+ else
+ {
+ // Rewind the current state by copying back the previous state.
+
+ BiquadFilterState.Span[0] = PreviousBiquadFilterState.Span[0];
+ }
+
+ if (HasVolumeRamp)
+ {
+ float volume = Volume0;
+ float ramp = (Volume1 - Volume0) / (int)context.SampleCount;
+
+ State.Span[0].LastSamples[LastSampleIndex] = BiquadFilterHelper.ProcessBiquadFilterAndMixRamp(
+ ref _parameter,
+ ref BiquadFilterState.Span[0],
+ outputBuffer,
+ inputBuffer,
+ context.SampleCount,
+ volume,
+ ramp);
+ }
+ else
+ {
+ BiquadFilterHelper.ProcessBiquadFilterAndMix(
+ ref _parameter,
+ ref BiquadFilterState.Span[0],
+ outputBuffer,
+ inputBuffer,
+ context.SampleCount,
+ Volume1);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs
index 098a04a04..de5c0ea2c 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs
@@ -30,8 +30,10 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
CopyMixBuffer,
LimiterVersion1,
LimiterVersion2,
- GroupedBiquadFilter,
+ MultiTapBiquadFilter,
CaptureBuffer,
Compressor,
+ BiquadFilterAndMix,
+ MultiTapBiquadFilterAndMix,
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs
index 09f415d20..33f61e6a5 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs
@@ -1,9 +1,11 @@
using Ryujinx.Audio.Renderer.Dsp.Effect;
using Ryujinx.Audio.Renderer.Dsp.State;
+using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Audio.Renderer.Parameter.Effect;
using Ryujinx.Audio.Renderer.Server.Effect;
using System;
using System.Diagnostics;
+using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Dsp.Command
{
@@ -21,18 +23,20 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
public CompressorParameter Parameter => _parameter;
public Memory State { get; }
+ public Memory ResultState { get; }
public ushort[] OutputBufferIndices { get; }
public ushort[] InputBufferIndices { get; }
public bool IsEffectEnabled { get; }
private CompressorParameter _parameter;
- public CompressorCommand(uint bufferOffset, CompressorParameter parameter, Memory state, bool isEnabled, int nodeId)
+ public CompressorCommand(uint bufferOffset, CompressorParameter parameter, Memory state, Memory resultState, bool isEnabled, int nodeId)
{
Enabled = true;
NodeId = nodeId;
_parameter = parameter;
State = state;
+ ResultState = resultState;
IsEffectEnabled = isEnabled;
@@ -71,9 +75,16 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
if (IsEffectEnabled && _parameter.IsChannelCountValid())
{
- Span inputBuffers = stackalloc IntPtr[Parameter.ChannelCount];
- Span outputBuffers = stackalloc IntPtr[Parameter.ChannelCount];
- Span channelInput = stackalloc float[Parameter.ChannelCount];
+ if (!ResultState.IsEmpty && _parameter.StatisticsReset)
+ {
+ ref CompressorStatistics statistics = ref MemoryMarshal.Cast(ResultState.Span[0].SpecificData)[0];
+
+ statistics.Reset(_parameter.ChannelCount);
+ }
+
+ Span inputBuffers = stackalloc IntPtr[_parameter.ChannelCount];
+ Span outputBuffers = stackalloc IntPtr[_parameter.ChannelCount];
+ Span channelInput = stackalloc float[_parameter.ChannelCount];
ExponentialMovingAverage inputMovingAverage = state.InputMovingAverage;
float unknown4 = state.Unknown4;
ExponentialMovingAverage compressionGainAverage = state.CompressionGainAverage;
@@ -92,7 +103,8 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
channelInput[channelIndex] = *((float*)inputBuffers[channelIndex] + sampleIndex);
}
- float newMean = inputMovingAverage.Update(FloatingPointHelper.MeanSquare(channelInput), _parameter.InputGain);
+ float mean = FloatingPointHelper.MeanSquare(channelInput);
+ float newMean = inputMovingAverage.Update(mean, _parameter.InputGain);
float y = FloatingPointHelper.Log10(newMean) * 10.0f;
float z = 1.0f;
@@ -111,7 +123,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
if (y >= state.Unknown14)
{
- tmpGain = ((1.0f / Parameter.Ratio) - 1.0f) * (y - Parameter.Threshold);
+ tmpGain = ((1.0f / _parameter.Ratio) - 1.0f) * (y - _parameter.Threshold);
}
else
{
@@ -126,7 +138,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
if ((unknown4 - z) <= 0.08f)
{
- compressionEmaAlpha = Parameter.ReleaseCoefficient;
+ compressionEmaAlpha = _parameter.ReleaseCoefficient;
if ((unknown4 - z) >= -0.08f)
{
@@ -140,18 +152,31 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
}
else
{
- compressionEmaAlpha = Parameter.AttackCoefficient;
+ compressionEmaAlpha = _parameter.AttackCoefficient;
}
float compressionGain = compressionGainAverage.Update(z, compressionEmaAlpha);
- for (int channelIndex = 0; channelIndex < Parameter.ChannelCount; channelIndex++)
+ for (int channelIndex = 0; channelIndex < _parameter.ChannelCount; channelIndex++)
{
*((float*)outputBuffers[channelIndex] + sampleIndex) = channelInput[channelIndex] * compressionGain * state.OutputGain;
}
unknown4 = unknown4New;
previousCompressionEmaAlpha = compressionEmaAlpha;
+
+ if (!ResultState.IsEmpty)
+ {
+ ref CompressorStatistics statistics = ref MemoryMarshal.Cast(ResultState.Span[0].SpecificData)[0];
+
+ statistics.MinimumGain = MathF.Min(statistics.MinimumGain, compressionGain * state.OutputGain);
+ statistics.MaximumMean = MathF.Max(statistics.MaximumMean, mean);
+
+ for (int channelIndex = 0; channelIndex < _parameter.ChannelCount; channelIndex++)
+ {
+ statistics.LastSamples[channelIndex] = MathF.Abs(channelInput[channelIndex] * (1f / 32768f));
+ }
+ }
}
state.InputMovingAverage = inputMovingAverage;
@@ -161,7 +186,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
}
else
{
- for (int i = 0; i < Parameter.ChannelCount; i++)
+ for (int i = 0; i < _parameter.ChannelCount; i++)
{
if (InputBufferIndices[i] != OutputBufferIndices[i])
{
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs
index 3ba0b5884..06e932199 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs
@@ -38,10 +38,10 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
InputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
OutputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
- for (int i = 0; i < Parameter.ChannelCount; i++)
+ for (int i = 0; i < _parameter.ChannelCount; i++)
{
- InputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Input[i]);
- OutputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Output[i]);
+ InputBufferIndices[i] = (ushort)(bufferOffset + _parameter.Input[i]);
+ OutputBufferIndices[i] = (ushort)(bufferOffset + _parameter.Output[i]);
}
}
@@ -51,11 +51,11 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
if (IsEffectEnabled)
{
- if (Parameter.Status == UsageState.Invalid)
+ if (_parameter.Status == UsageState.Invalid)
{
state = new LimiterState(ref _parameter, WorkBuffer);
}
- else if (Parameter.Status == UsageState.New)
+ else if (_parameter.Status == UsageState.New)
{
LimiterState.UpdateParameter(ref _parameter);
}
@@ -66,56 +66,56 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
private unsafe void ProcessLimiter(CommandList context, ref LimiterState state)
{
- Debug.Assert(Parameter.IsChannelCountValid());
+ Debug.Assert(_parameter.IsChannelCountValid());
- if (IsEffectEnabled && Parameter.IsChannelCountValid())
+ if (IsEffectEnabled && _parameter.IsChannelCountValid())
{
- Span inputBuffers = stackalloc IntPtr[Parameter.ChannelCount];
- Span outputBuffers = stackalloc IntPtr[Parameter.ChannelCount];
+ Span inputBuffers = stackalloc IntPtr[_parameter.ChannelCount];
+ Span outputBuffers = stackalloc IntPtr[_parameter.ChannelCount];
- for (int i = 0; i < Parameter.ChannelCount; i++)
+ for (int i = 0; i < _parameter.ChannelCount; i++)
{
inputBuffers[i] = context.GetBufferPointer(InputBufferIndices[i]);
outputBuffers[i] = context.GetBufferPointer(OutputBufferIndices[i]);
}
- for (int channelIndex = 0; channelIndex < Parameter.ChannelCount; channelIndex++)
+ for (int channelIndex = 0; channelIndex < _parameter.ChannelCount; channelIndex++)
{
for (int sampleIndex = 0; sampleIndex < context.SampleCount; sampleIndex++)
{
float rawInputSample = *((float*)inputBuffers[channelIndex] + sampleIndex);
- float inputSample = (rawInputSample / short.MaxValue) * Parameter.InputGain;
+ float inputSample = (rawInputSample / short.MaxValue) * _parameter.InputGain;
float sampleInputMax = Math.Abs(inputSample);
- float inputCoefficient = Parameter.ReleaseCoefficient;
+ float inputCoefficient = _parameter.ReleaseCoefficient;
if (sampleInputMax > state.DetectorAverage[channelIndex].Read())
{
- inputCoefficient = Parameter.AttackCoefficient;
+ inputCoefficient = _parameter.AttackCoefficient;
}
float detectorValue = state.DetectorAverage[channelIndex].Update(sampleInputMax, inputCoefficient);
float attenuation = 1.0f;
- if (detectorValue > Parameter.Threshold)
+ if (detectorValue > _parameter.Threshold)
{
- attenuation = Parameter.Threshold / detectorValue;
+ attenuation = _parameter.Threshold / detectorValue;
}
- float outputCoefficient = Parameter.ReleaseCoefficient;
+ float outputCoefficient = _parameter.ReleaseCoefficient;
if (state.CompressionGainAverage[channelIndex].Read() > attenuation)
{
- outputCoefficient = Parameter.AttackCoefficient;
+ outputCoefficient = _parameter.AttackCoefficient;
}
float compressionGain = state.CompressionGainAverage[channelIndex].Update(attenuation, outputCoefficient);
- ref float delayedSample = ref state.DelayedSampleBuffer[channelIndex * Parameter.DelayBufferSampleCountMax + state.DelayedSampleBufferPosition[channelIndex]];
+ ref float delayedSample = ref state.DelayedSampleBuffer[channelIndex * _parameter.DelayBufferSampleCountMax + state.DelayedSampleBufferPosition[channelIndex]];
- float outputSample = delayedSample * compressionGain * Parameter.OutputGain;
+ float outputSample = delayedSample * compressionGain * _parameter.OutputGain;
*((float*)outputBuffers[channelIndex] + sampleIndex) = outputSample * short.MaxValue;
@@ -123,16 +123,16 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
state.DelayedSampleBufferPosition[channelIndex]++;
- while (state.DelayedSampleBufferPosition[channelIndex] >= Parameter.DelayBufferSampleCountMin)
+ while (state.DelayedSampleBufferPosition[channelIndex] >= _parameter.DelayBufferSampleCountMin)
{
- state.DelayedSampleBufferPosition[channelIndex] -= Parameter.DelayBufferSampleCountMin;
+ state.DelayedSampleBufferPosition[channelIndex] -= _parameter.DelayBufferSampleCountMin;
}
}
}
}
else
{
- for (int i = 0; i < Parameter.ChannelCount; i++)
+ for (int i = 0; i < _parameter.ChannelCount; i++)
{
if (InputBufferIndices[i] != OutputBufferIndices[i])
{
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs
index f6e1654dd..ed0538c06 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs
@@ -49,10 +49,10 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
InputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
OutputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
- for (int i = 0; i < Parameter.ChannelCount; i++)
+ for (int i = 0; i < _parameter.ChannelCount; i++)
{
- InputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Input[i]);
- OutputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Output[i]);
+ InputBufferIndices[i] = (ushort)(bufferOffset + _parameter.Input[i]);
+ OutputBufferIndices[i] = (ushort)(bufferOffset + _parameter.Output[i]);
}
}
@@ -62,11 +62,11 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
if (IsEffectEnabled)
{
- if (Parameter.Status == UsageState.Invalid)
+ if (_parameter.Status == UsageState.Invalid)
{
state = new LimiterState(ref _parameter, WorkBuffer);
}
- else if (Parameter.Status == UsageState.New)
+ else if (_parameter.Status == UsageState.New)
{
LimiterState.UpdateParameter(ref _parameter);
}
@@ -77,63 +77,63 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
private unsafe void ProcessLimiter(CommandList context, ref LimiterState state)
{
- Debug.Assert(Parameter.IsChannelCountValid());
+ Debug.Assert(_parameter.IsChannelCountValid());
- if (IsEffectEnabled && Parameter.IsChannelCountValid())
+ if (IsEffectEnabled && _parameter.IsChannelCountValid())
{
- if (!ResultState.IsEmpty && Parameter.StatisticsReset)
+ if (!ResultState.IsEmpty && _parameter.StatisticsReset)
{
ref LimiterStatistics statistics = ref MemoryMarshal.Cast(ResultState.Span[0].SpecificData)[0];
statistics.Reset();
}
- Span inputBuffers = stackalloc IntPtr[Parameter.ChannelCount];
- Span outputBuffers = stackalloc IntPtr[Parameter.ChannelCount];
+ Span inputBuffers = stackalloc IntPtr[_parameter.ChannelCount];
+ Span outputBuffers = stackalloc IntPtr[_parameter.ChannelCount];
- for (int i = 0; i < Parameter.ChannelCount; i++)
+ for (int i = 0; i < _parameter.ChannelCount; i++)
{
inputBuffers[i] = context.GetBufferPointer(InputBufferIndices[i]);
outputBuffers[i] = context.GetBufferPointer(OutputBufferIndices[i]);
}
- for (int channelIndex = 0; channelIndex < Parameter.ChannelCount; channelIndex++)
+ for (int channelIndex = 0; channelIndex < _parameter.ChannelCount; channelIndex++)
{
for (int sampleIndex = 0; sampleIndex < context.SampleCount; sampleIndex++)
{
float rawInputSample = *((float*)inputBuffers[channelIndex] + sampleIndex);
- float inputSample = (rawInputSample / short.MaxValue) * Parameter.InputGain;
+ float inputSample = (rawInputSample / short.MaxValue) * _parameter.InputGain;
float sampleInputMax = Math.Abs(inputSample);
- float inputCoefficient = Parameter.ReleaseCoefficient;
+ float inputCoefficient = _parameter.ReleaseCoefficient;
if (sampleInputMax > state.DetectorAverage[channelIndex].Read())
{
- inputCoefficient = Parameter.AttackCoefficient;
+ inputCoefficient = _parameter.AttackCoefficient;
}
float detectorValue = state.DetectorAverage[channelIndex].Update(sampleInputMax, inputCoefficient);
float attenuation = 1.0f;
- if (detectorValue > Parameter.Threshold)
+ if (detectorValue > _parameter.Threshold)
{
- attenuation = Parameter.Threshold / detectorValue;
+ attenuation = _parameter.Threshold / detectorValue;
}
- float outputCoefficient = Parameter.ReleaseCoefficient;
+ float outputCoefficient = _parameter.ReleaseCoefficient;
if (state.CompressionGainAverage[channelIndex].Read() > attenuation)
{
- outputCoefficient = Parameter.AttackCoefficient;
+ outputCoefficient = _parameter.AttackCoefficient;
}
float compressionGain = state.CompressionGainAverage[channelIndex].Update(attenuation, outputCoefficient);
- ref float delayedSample = ref state.DelayedSampleBuffer[channelIndex * Parameter.DelayBufferSampleCountMax + state.DelayedSampleBufferPosition[channelIndex]];
+ ref float delayedSample = ref state.DelayedSampleBuffer[channelIndex * _parameter.DelayBufferSampleCountMax + state.DelayedSampleBufferPosition[channelIndex]];
- float outputSample = delayedSample * compressionGain * Parameter.OutputGain;
+ float outputSample = delayedSample * compressionGain * _parameter.OutputGain;
*((float*)outputBuffers[channelIndex] + sampleIndex) = outputSample * short.MaxValue;
@@ -141,9 +141,9 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
state.DelayedSampleBufferPosition[channelIndex]++;
- while (state.DelayedSampleBufferPosition[channelIndex] >= Parameter.DelayBufferSampleCountMin)
+ while (state.DelayedSampleBufferPosition[channelIndex] >= _parameter.DelayBufferSampleCountMin)
{
- state.DelayedSampleBufferPosition[channelIndex] -= Parameter.DelayBufferSampleCountMin;
+ state.DelayedSampleBufferPosition[channelIndex] -= _parameter.DelayBufferSampleCountMin;
}
if (!ResultState.IsEmpty)
@@ -158,7 +158,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
}
else
{
- for (int i = 0; i < Parameter.ChannelCount; i++)
+ for (int i = 0; i < _parameter.ChannelCount; i++)
{
if (InputBufferIndices[i] != OutputBufferIndices[i])
{
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/MixRampGroupedCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/MixRampGroupedCommand.cs
index 3c7dd63b2..41ac84c1a 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/MixRampGroupedCommand.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/MixRampGroupedCommand.cs
@@ -24,7 +24,14 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
public Memory State { get; }
- public MixRampGroupedCommand(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, Span volume0, Span volume1, Memory state, int nodeId)
+ public MixRampGroupedCommand(
+ uint mixBufferCount,
+ uint inputBufferIndex,
+ uint outputBufferIndex,
+ ReadOnlySpan volume0,
+ ReadOnlySpan volume1,
+ Memory state,
+ int nodeId)
{
Enabled = true;
MixBufferCount = mixBufferCount;
@@ -48,7 +55,12 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static float ProcessMixRampGrouped(Span outputBuffer, ReadOnlySpan inputBuffer, float volume0, float volume1, int sampleCount)
+ private static float ProcessMixRampGrouped(
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ float volume0,
+ float volume1,
+ int sampleCount)
{
float ramp = (volume1 - volume0) / sampleCount;
float volume = volume0;
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterAndMixCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterAndMixCommand.cs
new file mode 100644
index 000000000..e359371b4
--- /dev/null
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterAndMixCommand.cs
@@ -0,0 +1,145 @@
+using Ryujinx.Audio.Renderer.Common;
+using Ryujinx.Audio.Renderer.Dsp.State;
+using Ryujinx.Audio.Renderer.Parameter;
+using System;
+
+namespace Ryujinx.Audio.Renderer.Dsp.Command
+{
+ public class MultiTapBiquadFilterAndMixCommand : ICommand
+ {
+ public bool Enabled { get; set; }
+
+ public int NodeId { get; }
+
+ public CommandType CommandType => CommandType.MultiTapBiquadFilterAndMix;
+
+ public uint EstimatedProcessingTime { get; set; }
+
+ public ushort InputBufferIndex { get; }
+ public ushort OutputBufferIndex { get; }
+
+ private BiquadFilterParameter _parameter0;
+ private BiquadFilterParameter _parameter1;
+
+ public Memory BiquadFilterState0 { get; }
+ public Memory BiquadFilterState1 { get; }
+ public Memory PreviousBiquadFilterState0 { get; }
+ public Memory PreviousBiquadFilterState1 { get; }
+
+ public Memory State { get; }
+
+ public int LastSampleIndex { get; }
+
+ public float Volume0 { get; }
+ public float Volume1 { get; }
+
+ public bool NeedInitialization0 { get; }
+ public bool NeedInitialization1 { get; }
+ public bool HasVolumeRamp { get; }
+ public bool IsFirstMixBuffer { get; }
+
+ public MultiTapBiquadFilterAndMixCommand(
+ float volume0,
+ float volume1,
+ uint inputBufferIndex,
+ uint outputBufferIndex,
+ int lastSampleIndex,
+ Memory state,
+ ref BiquadFilterParameter filter0,
+ ref BiquadFilterParameter filter1,
+ Memory biquadFilterState0,
+ Memory biquadFilterState1,
+ Memory previousBiquadFilterState0,
+ Memory previousBiquadFilterState1,
+ bool needInitialization0,
+ bool needInitialization1,
+ bool hasVolumeRamp,
+ bool isFirstMixBuffer,
+ int nodeId)
+ {
+ Enabled = true;
+ NodeId = nodeId;
+
+ InputBufferIndex = (ushort)inputBufferIndex;
+ OutputBufferIndex = (ushort)outputBufferIndex;
+
+ _parameter0 = filter0;
+ _parameter1 = filter1;
+ BiquadFilterState0 = biquadFilterState0;
+ BiquadFilterState1 = biquadFilterState1;
+ PreviousBiquadFilterState0 = previousBiquadFilterState0;
+ PreviousBiquadFilterState1 = previousBiquadFilterState1;
+
+ State = state;
+ LastSampleIndex = lastSampleIndex;
+
+ Volume0 = volume0;
+ Volume1 = volume1;
+
+ NeedInitialization0 = needInitialization0;
+ NeedInitialization1 = needInitialization1;
+ HasVolumeRamp = hasVolumeRamp;
+ IsFirstMixBuffer = isFirstMixBuffer;
+ }
+
+ private void UpdateState(Memory state, Memory previousState, bool needInitialization)
+ {
+ if (needInitialization)
+ {
+ // If there is no previous state, initialize to zero.
+
+ state.Span[0] = new BiquadFilterState();
+ }
+ else if (IsFirstMixBuffer)
+ {
+ // This is the first buffer, set previous state to current state.
+
+ previousState.Span[0] = state.Span[0];
+ }
+ else
+ {
+ // Rewind the current state by copying back the previous state.
+
+ state.Span[0] = previousState.Span[0];
+ }
+ }
+
+ public void Process(CommandList context)
+ {
+ ReadOnlySpan inputBuffer = context.GetBuffer(InputBufferIndex);
+ Span outputBuffer = context.GetBuffer(OutputBufferIndex);
+
+ UpdateState(BiquadFilterState0, PreviousBiquadFilterState0, NeedInitialization0);
+ UpdateState(BiquadFilterState1, PreviousBiquadFilterState1, NeedInitialization1);
+
+ if (HasVolumeRamp)
+ {
+ float volume = Volume0;
+ float ramp = (Volume1 - Volume0) / (int)context.SampleCount;
+
+ State.Span[0].LastSamples[LastSampleIndex] = BiquadFilterHelper.ProcessDoubleBiquadFilterAndMixRamp(
+ ref _parameter0,
+ ref _parameter1,
+ ref BiquadFilterState0.Span[0],
+ ref BiquadFilterState1.Span[0],
+ outputBuffer,
+ inputBuffer,
+ context.SampleCount,
+ volume,
+ ramp);
+ }
+ else
+ {
+ BiquadFilterHelper.ProcessDoubleBiquadFilterAndMix(
+ ref _parameter0,
+ ref _parameter1,
+ ref BiquadFilterState0.Span[0],
+ ref BiquadFilterState1.Span[0],
+ outputBuffer,
+ inputBuffer,
+ context.SampleCount,
+ Volume1);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/GroupedBiquadFilterCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterCommand.cs
similarity index 84%
rename from src/Ryujinx.Audio/Renderer/Dsp/Command/GroupedBiquadFilterCommand.cs
rename to src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterCommand.cs
index 7af851bdc..e159f8ef7 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/GroupedBiquadFilterCommand.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterCommand.cs
@@ -4,13 +4,13 @@ using System;
namespace Ryujinx.Audio.Renderer.Dsp.Command
{
- public class GroupedBiquadFilterCommand : ICommand
+ public class MultiTapBiquadFilterCommand : ICommand
{
public bool Enabled { get; set; }
public int NodeId { get; }
- public CommandType CommandType => CommandType.GroupedBiquadFilter;
+ public CommandType CommandType => CommandType.MultiTapBiquadFilter;
public uint EstimatedProcessingTime { get; set; }
@@ -20,7 +20,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
private readonly int _outputBufferIndex;
private readonly bool[] _isInitialized;
- public GroupedBiquadFilterCommand(int baseIndex, ReadOnlySpan filters, Memory biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan isInitialized, int nodeId)
+ public MultiTapBiquadFilterCommand(int baseIndex, ReadOnlySpan filters, Memory biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan isInitialized, int nodeId)
{
_parameters = filters.ToArray();
_biquadFilterStates = biquadFilterStateMemory;
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs b/src/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs
index f9a32b3f9..58a2d9cce 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs
@@ -2,12 +2,16 @@ using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Dsp.State
{
- [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x10)]
+ [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x20)]
public struct BiquadFilterState
{
public float State0;
public float State1;
public float State2;
public float State3;
+ public float State4;
+ public float State5;
+ public float State6;
+ public float State7;
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Parameter/BehaviourErrorInfoOutStatus.cs b/src/Ryujinx.Audio/Renderer/Parameter/BehaviourErrorInfoOutStatus.cs
index 5a0565dc6..72438be0e 100644
--- a/src/Ryujinx.Audio/Renderer/Parameter/BehaviourErrorInfoOutStatus.cs
+++ b/src/Ryujinx.Audio/Renderer/Parameter/BehaviourErrorInfoOutStatus.cs
@@ -8,7 +8,7 @@ namespace Ryujinx.Audio.Renderer.Parameter
///
/// Output information for behaviour.
///
- /// This is used to report errors to the user during processing.
+ /// This is used to report errors to the user during processing.
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct BehaviourErrorInfoOutStatus
{
diff --git a/src/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs b/src/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs
index b403f1370..c00118e49 100644
--- a/src/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs
+++ b/src/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs
@@ -90,9 +90,16 @@ namespace Ryujinx.Audio.Renderer.Parameter.Effect
public bool MakeupGainEnabled;
///
- /// Reserved/padding.
+ /// Indicate if the compressor effect should output statistics.
///
- private Array2 _reserved;
+ [MarshalAs(UnmanagedType.I1)]
+ public bool StatisticsEnabled;
+
+ ///
+ /// Indicate to the DSP that the user did a statistics reset.
+ ///
+ [MarshalAs(UnmanagedType.I1)]
+ public bool StatisticsReset;
///
/// Check if the is valid.
diff --git a/src/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorStatistics.cs b/src/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorStatistics.cs
new file mode 100644
index 000000000..65335e2d9
--- /dev/null
+++ b/src/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorStatistics.cs
@@ -0,0 +1,38 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Audio.Renderer.Parameter.Effect
+{
+ ///
+ /// Effect result state for