` 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 1ff4cdc22..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,14 +145,13 @@ 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 supports DLC + Game Update Add-ons.
@@ -108,14 +161,13 @@ If having Issues installing firmware (Make sure your Keys are installed first)
The emulator has settings for enabling or disabling some logging, remapping controllers, and more.
-## 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 862c9e7b5..45f0688bf 100644
--- a/src/ARMeilleure/Translation/Cache/JitCache.cs
+++ b/src/ARMeilleure/Translation/Cache/JitCache.cs
@@ -117,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)
{
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 485f34a3a..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 = (
@@ -680,8 +703,96 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(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;
@@ -692,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",
@@ -779,10 +890,168 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/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.4.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;
@@ -792,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 = (
@@ -846,8 +1119,96 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(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;
@@ -858,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",
@@ -945,10 +1306,168 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/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.4.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;
@@ -958,6 +1477,7 @@
};
4E80A9B52CD6F54700029585 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -977,6 +1497,7 @@
};
4E80A9B62CD6F54700029585 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -996,6 +1517,7 @@
};
4E80A9B82CD6F54700029585 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
@@ -1013,6 +1535,7 @@
};
4E80A9B92CD6F54700029585 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
@@ -1030,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;
@@ -1046,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;
@@ -1058,6 +1585,7 @@
};
BD43C6232D1B248D003BBC42 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 95J8WZ4TN8;
@@ -1067,6 +1595,7 @@
};
BD43C6242D1B248D003BBC42 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 95J8WZ4TN8;
@@ -1134,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" */ = {
@@ -1153,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 c9eaf9e54..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 847a0691d..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);
@@ -50,12 +57,16 @@ 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 touch_began(float x, float y, int index);
diff --git a/src/MeloNX/MeloNX/App/Core/JIT/IsJITEnabled.swift b/src/MeloNX/MeloNX/App/Core/JIT/IsJITEnabled.swift
index 44fbf1a72..fd5b7c68e 100644
--- a/src/MeloNX/MeloNX/App/Core/JIT/IsJITEnabled.swift
+++ b/src/MeloNX/MeloNX/App/Core/JIT/IsJITEnabled.swift
@@ -20,6 +20,14 @@ func isJITEnabled() -> Bool {
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
@@ -34,7 +42,7 @@ func checkMemoryPermissions(at address: UnsafeRawPointer) -> Bool {
}
if result != KERN_SUCCESS {
- print("Failed to reach \(address)")
+ // print("Failed to reach \(address)")
return false
}
diff --git a/src/MeloNX/MeloNX/App/Core/JIT/JitStreamerEB/EnableJIT.swift b/src/MeloNX/MeloNX/App/Core/JIT/JitStreamerEB/EnableJIT.swift
index 37455a716..d1eecfda4 100644
--- a/src/MeloNX/MeloNX/App/Core/JIT/JitStreamerEB/EnableJIT.swift
+++ b/src/MeloNX/MeloNX/App/Core/JIT/JitStreamerEB/EnableJIT.swift
@@ -6,39 +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()
}
+}
+
+func enableJITEBRequest() {
+ let pid = Int(getpid())
+ // print(pid)
- let address = URL(string: "http://[fd00::]:9172/launch_app/\(bundleID)")!
+ 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: address) { data, response, error in
- if error != nil {
+ 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) {
@@ -47,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: .A)
- setupButtonChangeListener(gamepad.buttonB, for: .B)
- setupButtonChangeListener(gamepad.buttonX, for: .X)
- setupButtonChangeListener(gamepad.buttonY, for: .Y)
+
+ 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 b6d0510bc..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,96 +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)")
+ // print("Rumble with \(lowFreq), \(highFreq)")
if UIDevice.current.userInterfaceIdiom == .phone {
- VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
+ 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 {
- 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: [])
-
-
- 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: [])
-
- var engine = engine
-
- if engine == nil {
- if hapticEngine == nil {
- hapticEngine = try CHHapticEngine()
- try hapticEngine?.start()
- }
-
- engine = hapticEngine
- }
-
- guard let engine else {
- return print("Error creating haptic patterns: hapticEngine is nil")
- }
-
- let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
- try lowFreqPlayer.start(atTime: 0)
-
- let highFreqPlayer = try engine.makePlayer(with: highFreqPattern)
- try highFreqPlayer.start(atTime: 0)
-
- } 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)
@@ -133,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))
@@ -150,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
@@ -174,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
@@ -191,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/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 8d75f2231..3c09d1905 100644
--- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift
@@ -10,6 +10,94 @@ 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
@@ -30,61 +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: MeloMTKView? = nil
- @Published var config: Ryujinx.Configuration? = 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,
@@ -107,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
@@ -135,17 +269,71 @@ 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
}
self.config = config
- RunLoop.current.perform { [self] in
+
+ 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
@@ -165,7 +353,9 @@ class Ryujinx {
let result = main_ryujinx_sdl(Int32(args.count), &argvPtrs)
if result != 0 {
- self.isRunning = false
+ DispatchQueue.main.async {
+ self.isRunning = false
+ }
if let accessing, accessing {
url!.stopAccessingSecurityScopedResource()
}
@@ -174,13 +364,103 @@ class Ryujinx {
}
}
} 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
@@ -209,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] = []
@@ -234,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
@@ -258,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")
}
@@ -347,6 +647,16 @@ class Ryujinx {
}
}
+ // 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
@@ -360,18 +670,13 @@ class Ryujinx {
}
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"
@@ -379,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
}
@@ -395,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)))
@@ -452,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)
@@ -483,84 +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 {
- self.metalLayer = layer
- break
- }
-
- Thread.sleep(forTimeInterval: 0.1)
- }
- }
- }
-
-
- @MainActor
- func getMetalLayer(_ window: OpaquePointer?) -> CAMetalLayer? {
- var window = window
- if window == nil {
- window = SDL_GetWindowFromID(1)
- }
-
- var windowInfo = SDL_SysWMinfo()
- SDL_GetWindowWMInfo(window, &windowInfo)
-
-
- guard let uiWindow = windowInfo.info.uikit.window,
- let rootView = uiWindow.takeUnretainedValue().rootViewController?.view else {
- print("Unable to get root view")
- return nil
- }
-
- func findMetalLayer(in view: UIView) -> CAMetalLayer? {
- if let metalLayer = view.layer as? CAMetalLayer {
- return metalLayer
- }
-
- for subview in view.subviews {
- if let metalLayer = findMetalLayer(in: subview) {
- return metalLayer
- }
- }
-
- return nil
- }
-
- if let existingLayer = findMetalLayer(in: rootView) {
- print("Found Metal Layer")
- return existingLayer
- }
- print("found nothing")
- return nil
- }
-
static func log(_ message: String) {
- 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/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/Main/ContentView.swift b/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift
deleted file mode 100644
index 3e9bdb4b0..000000000
--- a/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift
+++ /dev/null
@@ -1,389 +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
- @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
- @State var jitNotEnabled = false
-
- // MARK: - Initialization
- init() {
- var defaultConfig = loadSettings()
- if defaultConfig == nil {
- saveSettings(config: .init(gamepath: ""))
-
- defaultConfig = loadSettings()
- }
-
-
- _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"),
- MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "0"),
- // Uses more ram but makes performance higher, may add an option in settings to change or enable / disable this value (default 64)
- MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "512"),
- ]
-
- _settings = State(initialValue: defaultSettings)
-
- initializeSDL()
- }
-
- // MARK: - Body
- var body: some View {
- if game != nil, !jitNotEnabled {
- // This is when the game starts to stop the animation
- ZStack {
- if #available(iOS 16, *) {
- EmulationView(startgame: $game)
- .persistentSystemOverlays(.hidden)
- } else {
- EmulationView(startgame: $game)
- }
-
- if isLoading {
- ZStack {
- Color.black
- .opacity(0.8)
- emulationView
- .ignoresSafeArea(.all)
- }
- .edgesIgnoringSafeArea(.all)
- .ignoresSafeArea(.all)
- }
- }
- } else if game != nil, ignoreJIT {
- ZStack {
- if #available(iOS 16, *) {
- EmulationView(startgame: $game)
- .persistentSystemOverlays(.hidden)
- } else {
- EmulationView(startgame: $game)
- }
-
- if isLoading {
- ZStack {
- Color.black
- .opacity(0.8)
- emulationView
- .ignoresSafeArea(.all)
- }
- .edgesIgnoringSafeArea(.all)
- .ignoresSafeArea(.all)
- }
- }
- } else if game != nil {
- Text("")
- .sheet(isPresented: $jitNotEnabled) {
- JITPopover() {
- jitNotEnabled = false
- }
- .interactiveDismissDisabled()
- }
- } else {
- // This is the main menu view that includes the Settings and the Game Selector
- mainMenuView
- .onAppear() {
- quits = false
-
- loadSettings()
-
- isLoading = true
-
- initControllerObservers() // This initializes the Controller Observers that refreshes the controller list when a new controller connecvts.
- }
- .onOpenURL() { url in
- if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
- components.host == "game" {
- if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
-
- game = Ryujinx.shared.games.first(where: { $0.titleId == text })
- } else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
- game = Ryujinx.shared.games.first(where: { $0.titleName == text })
- }
- }
- }
- }
-
- }
-
-
- private func initControllerObservers() {
- NotificationCenter.default.addObserver(
- forName: .GCControllerDidConnect,
- object: nil,
- queue: .main) { notification in
- if let controller = notification.object as? GCController {
- print("Controller connected: \(controller.productCategory)")
- nativeControllers[controller] = .init(controller)
- refreshControllersList()
- }
- }
-
-
- NotificationCenter.default.addObserver(
- forName: .GCControllerDidDisconnect,
- object: nil,
- queue: .main) { notification in
- if let controller = notification.object as? GCController {
- print("Controller disconnected: \(controller.productCategory)")
- nativeControllers[controller]?.cleanup()
- nativeControllers[controller] = nil
- refreshControllersList()
- }
- }
- }
-
- // MARK: - View Components
- 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()
- }
- }
- }
- }
- .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
- )
- }
-
- if showlogsloading {
- LogFileView(isfps: true)
- .frame(alignment: .topLeading)
- }
- }
- }
-
- private var mainMenuView: some View {
- MainTabView(startemu: $game, config: $config, MVKconfig: $settings, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
- .onAppear() {
- Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { timer in
- refreshControllersList()
- }
-
- Air.play(AnyView(
- VStack {
- Image(systemName: "gamecontroller")
- .font(.system(size: 300))
- .foregroundColor(.gray)
- .padding(.bottom, 10)
-
- Text("Select Game")
- .font(.system(size: 150))
- .bold()
- }
- ))
-
- jitNotEnabled = !isJITEnabled()
- if jitNotEnabled {
- useTrollStore ? askForJIT() : jitStreamerEB ? enableJITEB() : print("no JIT")
- }
- }
- }
-
- // MARK: - Helper Methods
- var SdlInitFlags: uint = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO; // Initialises SDL2 for Events, Game Controller, Joystick, Audio and Video.
- private func initializeSDL() {
- 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() {
- 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)
- }
- }
-}
-
-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/Main/ControllerView/ControllerView.swift b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/ControllerView.swift
deleted file mode 100644
index da922f31d..000000000
--- a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/ControllerView.swift
+++ /dev/null
@@ -1,290 +0,0 @@
-//
-// ControllerView.swift
-// Pomelo-V2
-//
-// Created by Stossy11 on 16/7/2024.
-//
-
-import SwiftUI
-import GameController
-import SwiftUIJoystick
-import CoreMotion
-
-struct ControllerView: View {
- var body: some View {
- GeometryReader { geometry in
- if geometry.size.height > geometry.size.width && UIDevice.current.userInterfaceIdiom != .pad {
- VStack {
-
- Spacer()
- VStack {
- HStack {
- VStack {
- ShoulderButtonsViewLeft()
- ZStack {
- Joystick()
- DPadView()
- }
- }
- Spacer()
- VStack {
- ShoulderButtonsViewRight()
- ZStack {
- Joystick(iscool: true) // hope this works
- ABXYView()
- }
- }
- }
-
- HStack {
- ButtonView(button: .start) // Adding the + button
- .padding(.horizontal, 40)
- ButtonView(button: .back) // Adding the - button
- .padding(.horizontal, 40)
- }
- }
- }
-
- } else {
- // could be landscape
- VStack {
-
- Spacer()
- VStack {
- HStack {
-
- // gotta fuckin add + and - now
- VStack {
- ShoulderButtonsViewLeft()
- ZStack {
- Joystick()
- DPadView()
- }
- }
- HStack {
- // Spacer()
- VStack {
- // Spacer()
- ButtonView(button: .back) // Adding the - button
- }
- Spacer()
- VStack {
- // Spacer()
- ButtonView(button: .start) // Adding the + button
- }
- // Spacer()
- }
- VStack {
- ShoulderButtonsViewRight()
- ZStack {
- Joystick(iscool: true) // hope this work s
- ABXYView()
- }
- }
- }
-
- }
- // .padding(.bottom, geometry.size.height / 11) // also extremally broken (
- }
- }
- }
- .padding()
- }
-}
-
-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/Main/ControllerView/Joystick/JoystickView.swift b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/JoystickView.swift
deleted file mode 100644
index 7747719c2..000000000
--- a/src/MeloNX/MeloNX/App/Views/Main/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/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/Main/Emulation/AirPlay/Air.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/Air.swift
index 8842c50ea..4230ffae1 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/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/Main/Emulation/AirPlay/AirPlay.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/AirPlay.swift
index a3c90b241..0eeb7c835 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/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/Main/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/Main/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/Main/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
index 5e1a3279e..58a2b6d49 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/EmulationView.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/EmulationView.swift
@@ -14,11 +14,20 @@ struct EmulationView: View {
@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 {
@@ -26,8 +35,13 @@ struct EmulationView: View {
.ignoresSafeArea()
.edgesIgnoringSafeArea(.all)
.onAppear {
- Air.play(AnyView(MetalView().ignoresSafeArea()))
+ Air.play(AnyView(MetalView().ignoresSafeArea().edgesIgnoringSafeArea(.all)))
}
+
+ Color.black
+ .ignoresSafeArea()
+ .edgesIgnoringSafeArea(.all)
+ .allowsHitTesting(false)
} else {
MetalView() // The Emulation View
.ignoresSafeArea()
@@ -38,6 +52,8 @@ struct EmulationView: View {
if isVCA {
ControllerView() // Virtual Controller
+ .opacity(controllerOpacity)
+ .allowsHitTesting(true)
}
Group {
@@ -62,38 +78,98 @@ struct EmulationView: View {
Spacer()
}
- Spacer()
if ssb {
HStack {
- Button {
- if let screenshot = Ryujinx.shared.emulationUIView?.screenshot() {
- UIImageWriteToSavedPhotosAlbum(screenshot, nil, nil, nil)
+ 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: {
- Image(systemName: "square.and.arrow.up")
+ ExtButtonIconView(button: .guide, opacity: 0.4)
}
- .frame(width: UIDevice.current.systemName.contains("iPadOS") ? 60 * 1.2 : 45, height: UIDevice.current.systemName.contains("iPadOS") ? 60 * 1.2 : 45)
.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
index 6c4625e19..25d296012 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MeloMTKView.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MeloMTKView.swift
@@ -9,10 +9,10 @@ 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
@@ -84,71 +84,112 @@ class MeloMTKView: MTKView {
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)
- if scaleToTargetResolution(location) == nil {
+ guard let scaledLocation = scaleToTargetResolution(location) else {
ignoredTouches.insert(touch)
continue
}
+ let index = getNextAvailableIndex()
+ touchIndexMap[touch] = index
activeTouches.append(touch)
- let index = activeTouches.firstIndex(of: touch)!
- let scaledLocation = scaleToTargetResolution(location)!
- print("Touch began at: \(scaledLocation) and \(self.aspectRatio)")
touch_began(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index))
}
}
override func touchesEnded(_ touches: Set, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
-
- setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
-
- for touch in touches {
- if ignoredTouches.contains(touch) {
+
+ 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 index = activeTouches.firstIndex(of: touch) {
- activeTouches.remove(at: index)
+ if let touchIndex = touchIndexMap[touch] {
+ touch_ended(Int32(touchIndex))
- print("Touch ended for index \(index)")
- touch_ended(Int32(index))
+ 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
}
- let location = touch.location(in: self)
- guard let scaledLocation = scaleToTargetResolution(location) else {
- if let index = activeTouches.firstIndex(of: touch) {
- activeTouches.remove(at: index)
- print("Touch left active area, removed index \(index)")
- touch_ended(Int32(index))
- }
+ guard let touchIndex = touchIndexMap[touch] else {
continue
}
-
- if let index = activeTouches.firstIndex(of: touch) {
- print("Touch moved to: \(scaledLocation)")
- touch_moved(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index))
+
+ 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
index 44d786d25..9f9576bb0 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MetalView.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MetalView.swift
@@ -15,7 +15,7 @@ struct MetalView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
if Ryujinx.shared.emulationUIView == nil {
- var view = MeloMTKView()
+ let view = MeloMTKView()
guard let metalLayer = view.layer as? CAMetalLayer else {
fatalError("[Swift] Error: MTKView's layer is not a CAMetalLayer")
@@ -34,13 +34,19 @@ struct MetalView: UIViewRepresentable {
return view
}
- let uiview = UIView()
-
- uiview.layer.addSublayer(Ryujinx.shared.metalLayer!)
-
- uiview.contentScaleFactor = Ryujinx.shared.metalLayer!.contentsScale
-
- return uiview
+ 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) {
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/TouchView.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/TouchView.swift
index 03a85547f..e4cbaa44e 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/TouchView.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/TouchView.swift
@@ -10,7 +10,7 @@ import MetalKit
struct TouchView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
- var view = MeloMTKView()
+ let view = MeloMTKView()
return view
}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift b/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift
deleted file mode 100644
index eab988044..000000000
--- a/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift
+++ /dev/null
@@ -1,504 +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 = true
-
- isImporting = true
- } 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 {
- isSelectingGameFile = false
-
- isImporting = true
- } label: {
- Text("Open Game")
- }
-
- 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: [.folder, .nsp, .xci, .zip, .item]) { result in
- if isSelectingGameFile {
- 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)")
- }
-
- } else {
-
- switch result {
- case .success(let url):
- guard 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)
-
- 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)")
- }
- }
- }
- .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/Main/Logging/Logs.swift b/src/MeloNX/MeloNX/App/Views/Main/Logging/Logs.swift
deleted file mode 100644
index 1dc9c1549..000000000
--- a/src/MeloNX/MeloNX/App/Views/Main/Logging/Logs.swift
+++ /dev/null
@@ -1,117 +0,0 @@
-//
-// LogEntry.swift
-// MeloNX
-//
-// Created by Stossy11 on 09/02/2025.
-//
-
-import SwiftUI
-
-struct LogFileView: View {
- @State private var logs: [String] = []
- @State private var showingLogs = false
-
- public var isfps: Bool
-
- private let fileManager = FileManager.default
- private let maxDisplayLines = 10
-
- private var dateFormatter: DateFormatter {
- let formatter = DateFormatter()
- formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
- return formatter
- }
-
- var body: some View {
- VStack(alignment: .leading, spacing: 4) {
- ForEach(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)
- }
- }
- .onAppear {
- startLogFileWatching()
- }
- .onChange(of: logs) { newLogs in
- print("Logs updated: \(newLogs.count) entries")
- }
- }
-
- private func getLatestLogFile() -> URL? {
- let logsDirectory = URL.documentsDirectory.appendingPathComponent("Logs")
- let currentDate = Date()
-
- do {
- try fileManager.createDirectory(at: logsDirectory, withIntermediateDirectories: true)
-
- let logFiles = try fileManager.contentsOfDirectory(at: logsDirectory, includingPropertiesForKeys: [.creationDateKey])
- .filter {
- let filename = $0.lastPathComponent
- guard filename.hasPrefix("Ryujinx_ios_") && filename.hasSuffix(".log") else {
- return false
- }
-
- let dateString = filename.replacingOccurrences(of: "Ryujinx_ios_", with: "").replacingOccurrences(of: ".log", with: "")
- guard let logDate = dateFormatter.date(from: dateString) else {
- return false
- }
-
- return Calendar.current.isDate(logDate, inSameDayAs: currentDate)
- }
-
- let sortedLogFiles = logFiles.sorted {
- $0.lastPathComponent > $1.lastPathComponent
- }
-
- return sortedLogFiles.first
- } catch {
- print("Error finding log files: \(error)")
- return nil
- }
- }
-
- private func readLatestLogFile() {
- guard let logFileURL = getLatestLogFile() else {
- print("no logs?")
- return
- }
- print(logFileURL)
-
- do {
- let logContents = try String(contentsOf: logFileURL)
- let allLines = logContents.components(separatedBy: .newlines)
-
- DispatchQueue.global(qos: .userInteractive).async {
- self.logs = Array(allLines)
- }
- } catch {
- print("Error reading log file: \(error)")
- }
- }
-
- private func startLogFileWatching() {
- showingLogs = true
- self.readLatestLogFile()
- Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
- if showingLogs {
- self.readLatestLogFile()
- }
-
- if isfps {
- if get_current_fps() != 0 {
- stopLogFileWatching()
- timer.invalidate()
- }
- }
- }
- }
-
- private func stopLogFileWatching() {
- showingLogs = false
- }
-}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift b/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift
deleted file mode 100644
index 20623a3ba..000000000
--- a/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift
+++ /dev/null
@@ -1,765 +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
-
- @AppStorage("hasbeenfinished") var finishedStorage: Bool = false
-
- @AppStorage("showlogsloading") var showlogsloading: Bool = true
-
- @AppStorage("showlogsgame") var showlogsgame: Bool = false
-
- @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.handHeldController) {
- 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 gpuInfo = getGPUInfo(), gpuInfo.hasPrefix("Apple M") {
- if #available (iOS 16.4, *) {
- Toggle(isOn: .constant(false)) {
- labelWithIcon("Hypervisor", iconName: "bolt")
- }
- .tint(.blue)
- .disabled(true)
- .onAppear() {
- print("CPU Info: \(gpuInfo)")
- }
- } else if checkAppEntitlement("com.apple.private.hypervisor") {
- Toggle(isOn: $config.hypervisor) {
- labelWithIcon("Hypervisor", iconName: "bolt")
- }
- .tint(.blue)
- .onAppear() {
- print("CPU Info: \(gpuInfo)")
- }
- }
- }
- } 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: $showlogsloading) {
- labelWithIcon("Show logs while loading", iconName: "text.alignleft")
- }.tint(.blue)
-
- Toggle(isOn: $showlogsgame) {
- labelWithIcon("Show logs in-game", iconName: "text.line.magnify")
- }.tint(.blue)
-
- 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)
-
- if ProcessInfo.processInfo.isiOSAppOnMac {
- labelWithIcon("Device Memory: \(String(format: "%.0f GB", Double(totalMemory) / (1024 * 1024 * 1024)))", iconName: "memorychip.fill")
- } else {
- labelWithIcon("Device Memory: \(String(format: "%.0f GB", (Double(totalMemory) / (1024 * 1024 * 1024) + 1)))", 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 {
- 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)
-
- }
-
- Toggle(isOn: $ignoreJIT) {
- labelWithIcon("Ignore JIT Popup", iconName: "cpu")
- }.tint(.blue)
-
- 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 {
- finishedStorage = false
-
- } label: {
- Text("Show Setup")
- .font(.body)
- }
-
-
- } label: {
- Text("Advanced Options")
- }
- } header: {
- Text("Advanced")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- } footer: {
- Text("For advanced users. See page size or add custom arguments for experimental features. (Please don't touch this if you don't know what you're doing). \n \n\(gamepo ? "the cake is a lie" : "")")
- }
-
- }
- .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
- .navigationTitle("Settings")
- .navigationBarTitleDisplayMode(.inline)
- .listStyle(.insetGrouped)
- .onAppear {
- if let configs = loadSettings() {
- self.config = configs
- } else {
- saveSettings()
- }
- }
- .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 saveSettings() {
- MeloNX.saveSettings(config: config)
- }
-
- 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 getGPUInfo() -> String? {
- let device = MTLCreateSystemDefaultDevice()
-
- let gpu = device?.name
- print("GPU: " + (gpu ?? ""))
- return gpu
- }
-
-
- @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
- 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.Configuration) {
- 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.Configuration? {
- 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.Configuration.self, from: data)
- return configs
- } catch {
- print("Failed to load settings: \(error)")
- return nil
- }
-}
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/Main/GamesList/GameInfoSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameInfoSheet.swift
similarity index 85%
rename from src/MeloNX/MeloNX/App/Views/Main/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/Main/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/JIT/JITPopover.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/JIT/JITPopover.swift
similarity index 85%
rename from src/MeloNX/MeloNX/App/Views/Main/JIT/JITPopover.swift
rename to src/MeloNX/MeloNX/App/Views/Main/UI/JIT/JITPopover.swift
index 8c612b624..b779a0381 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/JIT/JITPopover.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/JIT/JITPopover.swift
@@ -9,7 +9,7 @@ import SwiftUI
struct JITPopover: View {
var onJITEnabled: () -> Void
- @Environment(\.dismiss) var dismiss
+ @Environment(\.presentationMode) var presentationMode
@State var isJIT: Bool = false
var body: some View {
@@ -35,8 +35,10 @@ struct JITPopover: View {
if isJIT {
- dismiss()
+ 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/Main/TabView/TabView.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/TabView/TabView.swift
similarity index 66%
rename from src/MeloNX/MeloNX/App/Views/Main/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/Main/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/Main/Updates/GameDLCManagerSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/Updates/GameDLCManagerSheet.swift
deleted file mode 100644
index 589c16545..000000000
--- a/src/MeloNX/MeloNX/App/Views/Main/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/Main/Updates/GameUpdateManagerSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/Updates/GameUpdateManagerSheet.swift
deleted file mode 100644
index 6ea8f6ceb..000000000
--- a/src/MeloNX/MeloNX/App/Views/Main/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/App/Views/MeloNXApp.swift b/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift
index 4f36da0b4..aeb7e5146 100644
--- a/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift
+++ b/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift
@@ -8,8 +8,15 @@
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 {
@@ -18,261 +25,126 @@ struct MeloNXApp: App {
@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 {
- ZStack {
- if showed || DRM != 1 {
-
- if finishedStorage {
- ContentView()
- } else {
- SetupView(finished: $finished)
- .onChange(of: finished) { newValue in
- withAnimation {
- withAnimation {
- finishedStorage = newValue
- }
+ 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 {
- Group {
- VStack {
- Spacer()
-
- HStack {
- Text("Loading...")
- ProgressView()
+ SetupView(finished: $finished)
+ .onChange(of: finished) { newValue in
+ withAnimation(.easeOut) {
+ finishedStorage = newValue
}
- 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)
+ .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
}
}
-
- // Start the task
- task.resume()
}
- } else {
- completion(false)
+ .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")
+ }
}
- } 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)
- }
+ 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 httpResponse = response as? HTTPURLResponse else {
- completion(false)
+ guard let url = URL(string: urlString) else {
+ // print("Invalid URL")
return
}
- if httpResponse.statusCode == 200 {
- completion(true)
- } else {
- completion(false)
+ 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)")
+ }
}
- 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)
+
+ 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/Setup/SetupView.swift b/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift
index e71e1ac10..50caa99aa 100644
--- a/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift
+++ b/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift
@@ -21,7 +21,7 @@ struct SetupView: View {
var body: some View {
iOSNav {
ZStack {
- if UIDevice.current.userInterfaceIdiom == .pad {
+ if UIDevice.current.systemName.contains("iPadOS") {
iPadSetupView(
finished: $finished,
isImportingKeys: $isImportingKeys,
@@ -54,18 +54,25 @@ struct SetupView: View {
) { result in
handleFirmwareImport(result: result)
}
- .alert(alertMessage, isPresented: $showAlert) {
- Button("OK", role: .cancel) {}
+ .alert(isPresented: $showAlert) {
+ Alert(title: Text(alertMessage), dismissButton: .default(Text("OK")))
}
- .alert("Skip Setup?", isPresented: $showSkipAlert) {
- Button("Skip", role: .destructive) { finished = true }
- Button("Cancel", role: .cancel) {}
+ .alert(isPresented: $showSkipAlert) {
+ Alert(
+ title: Text("Skip Setup?"),
+ primaryButton: .destructive(Text("Skip")) {
+ finished = true
+ },
+ secondaryButton: .cancel()
+ )
}
.onAppear {
initialize()
finished = false
keysImported = Ryujinx.shared.checkIfKeysImported()
- firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0")
+
+ let firmware = Ryujinx.shared.fetchFirmwareVersion()
+ firmImported = (firmware == "" ? "0" : firmware) != "0"
}
}
@@ -116,6 +123,9 @@ struct SetupView: View {
.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)
@@ -365,8 +375,8 @@ struct SetupView: View {
Ryujinx.shared.installFirmware(firmwarePath: fileURL.path)
-
- firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0")
+ let firmware = Ryujinx.shared.fetchFirmwareVersion()
+ firmImported = (firmware == "" ? "0" : firmware) != "0"
alertMessage = "Firmware installed successfully"
showAlert = true
@@ -385,7 +395,7 @@ struct SetupView: View {
let iconFileName = iconFiles.last else {
- print("Could not find icons in bundle")
+ // print("Could not find icons in bundle")
return ""
}
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/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 .
+ ///
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct CompressorStatistics
+ {
+ ///
+ /// Maximum input mean value since last reset.
+ ///
+ public float MaximumMean;
+
+ ///
+ /// Minimum output gain since last reset.
+ ///
+ public float MinimumGain;
+
+ ///
+ /// Last processed input sample, per channel.
+ ///
+ public Array6 LastSamples;
+
+ ///
+ /// Reset the statistics.
+ ///
+ /// Number of channels to reset.
+ public void Reset(ushort channelCount)
+ {
+ MaximumMean = 0.0f;
+ MinimumGain = 1.0f;
+ LastSamples.AsSpan()[..channelCount].Clear();
+ }
+ }
+}
diff --git a/src/Ryujinx.Audio/Renderer/Parameter/ISplitterDestinationInParameter.cs b/src/Ryujinx.Audio/Renderer/Parameter/ISplitterDestinationInParameter.cs
new file mode 100644
index 000000000..7ee49f11a
--- /dev/null
+++ b/src/Ryujinx.Audio/Renderer/Parameter/ISplitterDestinationInParameter.cs
@@ -0,0 +1,48 @@
+using Ryujinx.Common.Memory;
+using System;
+
+namespace Ryujinx.Audio.Renderer.Parameter
+{
+ ///
+ /// Generic interface for the splitter destination parameters.
+ ///
+ public interface ISplitterDestinationInParameter
+ {
+ ///
+ /// Target splitter destination data id.
+ ///
+ int Id { get; }
+
+ ///
+ /// The mix to output the result of the splitter.
+ ///
+ int DestinationId { get; }
+
+ ///
+ /// Biquad filter parameters.
+ ///
+ Array2 BiquadFilters { get; }
+
+ ///
+ /// Set to true if in use.
+ ///
+ bool IsUsed { get; }
+
+ ///
+ /// Set to true to force resetting the previous mix volumes.
+ ///
+ bool ResetPrevVolume { get; }
+
+ ///
+ /// Mix buffer volumes.
+ ///
+ /// Used when a splitter id is specified in the mix.
+ Span MixBufferVolume { get; }
+
+ ///
+ /// Check if the magic is valid.
+ ///
+ /// Returns true if the magic is valid.
+ bool IsMagicValid();
+ }
+}
diff --git a/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameter.cs b/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion1.cs
similarity index 63%
rename from src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameter.cs
rename to src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion1.cs
index b74b67be0..f346efcb0 100644
--- a/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameter.cs
+++ b/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion1.cs
@@ -1,3 +1,4 @@
+using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities;
using System;
using System.Runtime.InteropServices;
@@ -5,10 +6,10 @@ using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Parameter
{
///
- /// Input header for a splitter destination update.
+ /// Input header for a splitter destination version 1 update.
///
[StructLayout(LayoutKind.Sequential, Pack = 1)]
- public struct SplitterDestinationInParameter
+ public struct SplitterDestinationInParameterVersion1 : ISplitterDestinationInParameter
{
///
/// Magic of the input header.
@@ -36,12 +37,18 @@ namespace Ryujinx.Audio.Renderer.Parameter
[MarshalAs(UnmanagedType.I1)]
public bool IsUsed;
+ ///
+ /// Set to true to force resetting the previous mix volumes.
+ ///
+ [MarshalAs(UnmanagedType.I1)]
+ public bool ResetPrevVolume;
+
///
/// Reserved/padding.
///
- private unsafe fixed byte _reserved[3];
+ private unsafe fixed byte _reserved[2];
- [StructLayout(LayoutKind.Sequential, Size = 4 * Constants.MixBufferCountMax, Pack = 1)]
+ [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
private struct MixArray { }
///
@@ -50,6 +57,15 @@ namespace Ryujinx.Audio.Renderer.Parameter
/// Used when a splitter id is specified in the mix.
public Span MixBufferVolume => SpanHelpers.AsSpan(ref _mixBufferVolume);
+ readonly int ISplitterDestinationInParameter.Id => Id;
+
+ readonly int ISplitterDestinationInParameter.DestinationId => DestinationId;
+
+ readonly Array2 ISplitterDestinationInParameter.BiquadFilters => default;
+
+ readonly bool ISplitterDestinationInParameter.IsUsed => IsUsed;
+ readonly bool ISplitterDestinationInParameter.ResetPrevVolume => ResetPrevVolume;
+
///
/// The expected constant of any input header.
///
diff --git a/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion2.cs b/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion2.cs
new file mode 100644
index 000000000..1d867919d
--- /dev/null
+++ b/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion2.cs
@@ -0,0 +1,88 @@
+using Ryujinx.Common.Memory;
+using Ryujinx.Common.Utilities;
+using System;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Audio.Renderer.Parameter
+{
+ ///
+ /// Input header for a splitter destination version 2 update.
+ ///
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct SplitterDestinationInParameterVersion2 : ISplitterDestinationInParameter
+ {
+ ///
+ /// Magic of the input header.
+ ///
+ public uint Magic;
+
+ ///
+ /// Target splitter destination data id.
+ ///
+ public int Id;
+
+ ///
+ /// Mix buffer volumes storage.
+ ///
+ private MixArray _mixBufferVolume;
+
+ ///
+ /// The mix to output the result of the splitter.
+ ///
+ public int DestinationId;
+
+ ///
+ /// Biquad filter parameters.
+ ///
+ public Array2 BiquadFilters;
+
+ ///
+ /// Set to true if in use.
+ ///
+ [MarshalAs(UnmanagedType.I1)]
+ public bool IsUsed;
+
+ ///
+ /// Set to true to force resetting the previous mix volumes.
+ ///
+ [MarshalAs(UnmanagedType.I1)]
+ public bool ResetPrevVolume;
+
+ ///
+ /// Reserved/padding.
+ ///
+ private unsafe fixed byte _reserved[10];
+
+ [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
+ private struct MixArray { }
+
+ ///
+ /// Mix buffer volumes.
+ ///
+ /// Used when a splitter id is specified in the mix.
+ public Span MixBufferVolume => SpanHelpers.AsSpan(ref _mixBufferVolume);
+
+ readonly int ISplitterDestinationInParameter.Id => Id;
+
+ readonly int ISplitterDestinationInParameter.DestinationId => DestinationId;
+
+ readonly Array2 ISplitterDestinationInParameter.BiquadFilters => BiquadFilters;
+
+ readonly bool ISplitterDestinationInParameter.IsUsed => IsUsed;
+ readonly bool ISplitterDestinationInParameter.ResetPrevVolume => ResetPrevVolume;
+
+ ///