forked from MeloNX/MeloNX
Compare commits
No commits in common. "1f243ef21b40e8e8a26c070b05a99630d1e06250" and "fc68e3d4132e39ebb386e77bf2bb49376c989522" have entirely different histories.
1f243ef21b
...
fc68e3d413
@ -3,7 +3,6 @@ using ARMeilleure.CodeGen.Unwinding;
|
|||||||
using ARMeilleure.Memory;
|
using ARMeilleure.Memory;
|
||||||
using ARMeilleure.Native;
|
using ARMeilleure.Native;
|
||||||
using Ryujinx.Memory;
|
using Ryujinx.Memory;
|
||||||
using Ryujinx.Common.Logging;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -16,73 +15,52 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
static partial class JitCache
|
static partial class JitCache
|
||||||
{
|
{
|
||||||
private static readonly int _pageSize = (int)MemoryBlock.GetPageSize();
|
private static readonly int _pageSize = (int)MemoryBlock.GetPageSize();
|
||||||
private static readonly int _pageMask = _pageSize - 4;
|
private static readonly int _pageMask = _pageSize - 1;
|
||||||
|
|
||||||
private const int CodeAlignment = 4; // Bytes.
|
private const int CodeAlignment = 4; // Bytes.
|
||||||
private const int CacheSize = 128 * 1024 * 1024;
|
private const int CacheSize = 2047 * 1024 * 1024;
|
||||||
private const int CacheSizeIOS = 128 * 1024 * 1024;
|
private const int CacheSizeIOS = 1024 * 1024 * 1024;
|
||||||
|
|
||||||
private static ReservedRegion _jitRegion;
|
private static ReservedRegion _jitRegion;
|
||||||
private static JitCacheInvalidation _jitCacheInvalidator;
|
private static JitCacheInvalidation _jitCacheInvalidator;
|
||||||
|
|
||||||
private static List<CacheMemoryAllocator> _cacheAllocators = [];
|
private static CacheMemoryAllocator _cacheAllocator;
|
||||||
|
|
||||||
private static readonly List<CacheEntry> _cacheEntries = new();
|
private static readonly List<CacheEntry> _cacheEntries = new();
|
||||||
|
|
||||||
private static readonly object _lock = new();
|
private static readonly object _lock = new();
|
||||||
private static bool _initialized;
|
private static bool _initialized;
|
||||||
|
|
||||||
private static readonly List<ReservedRegion> _jitRegions = new();
|
|
||||||
|
|
||||||
private static int _activeRegionIndex = 0;
|
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
[SupportedOSPlatform("windows")]
|
||||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
public static partial IntPtr FlushInstructionCache(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize);
|
public static partial IntPtr FlushInstructionCache(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize);
|
||||||
|
|
||||||
public static void Initialize(IJitMemoryAllocator allocator)
|
public static void Initialize(IJitMemoryAllocator allocator)
|
||||||
{
|
{
|
||||||
|
if (_initialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
if (_initialized)
|
if (_initialized)
|
||||||
{
|
{
|
||||||
if (OperatingSystem.IsWindows())
|
return;
|
||||||
{
|
|
||||||
// JitUnwindWindows.RemoveFunctionTableHandler(
|
|
||||||
// _jitRegions[0].Pointer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < _jitRegions.Count; i++)
|
_jitRegion = new ReservedRegion(allocator, (ulong)(OperatingSystem.IsIOS() ? CacheSizeIOS : CacheSize));
|
||||||
{
|
|
||||||
_jitRegions[i].Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_jitRegions.Clear();
|
|
||||||
_cacheAllocators.Clear();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_activeRegionIndex = 0;
|
|
||||||
|
|
||||||
var firstRegion = new ReservedRegion(allocator, CacheSize);
|
|
||||||
_jitRegions.Add(firstRegion);
|
|
||||||
|
|
||||||
CacheMemoryAllocator firstCacheAllocator = new(CacheSize);
|
|
||||||
_cacheAllocators.Add(firstCacheAllocator);
|
|
||||||
|
|
||||||
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS())
|
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS())
|
||||||
{
|
{
|
||||||
_jitCacheInvalidator = new JitCacheInvalidation(allocator);
|
_jitCacheInvalidator = new JitCacheInvalidation(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
|
||||||
|
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
JitUnwindWindows.InstallFunctionTableHandler(
|
JitUnwindWindows.InstallFunctionTableHandler(_jitRegion.Pointer, CacheSize, _jitRegion.Pointer + Allocate(_pageSize));
|
||||||
firstRegion.Pointer, CacheSize, firstRegion.Pointer + Allocate(_pageSize)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
@ -95,9 +73,7 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
{
|
{
|
||||||
while (_deferredRxProtect.TryDequeue(out var result))
|
while (_deferredRxProtect.TryDequeue(out var result))
|
||||||
{
|
{
|
||||||
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
|
ReprotectAsExecutable(result.funcOffset, result.length);
|
||||||
|
|
||||||
ReprotectAsExecutable(targetRegion, result.funcOffset, result.length);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,8 +87,7 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
|
|
||||||
int funcOffset = Allocate(code.Length, deferProtect);
|
int funcOffset = Allocate(code.Length, deferProtect);
|
||||||
|
|
||||||
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
|
IntPtr funcPtr = _jitRegion.Pointer + funcOffset;
|
||||||
IntPtr funcPtr = targetRegion.Pointer + funcOffset;
|
|
||||||
|
|
||||||
if (OperatingSystem.IsIOS())
|
if (OperatingSystem.IsIOS())
|
||||||
{
|
{
|
||||||
@ -123,7 +98,8 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
|
ReprotectAsExecutable(funcOffset, code.Length);
|
||||||
|
|
||||||
JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length);
|
JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,9 +115,9 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ReprotectAsWritable(targetRegion, funcOffset, code.Length);
|
ReprotectAsWritable(funcOffset, code.Length);
|
||||||
Marshal.Copy(code, 0, funcPtr, code.Length);
|
Marshal.Copy(code, 0, funcPtr, code.Length);
|
||||||
ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
|
ReprotectAsExecutable(funcOffset, code.Length);
|
||||||
|
|
||||||
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||||
{
|
{
|
||||||
@ -163,50 +139,41 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
{
|
{
|
||||||
if (OperatingSystem.IsIOS())
|
if (OperatingSystem.IsIOS())
|
||||||
{
|
{
|
||||||
// return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
foreach (var region in _jitRegions)
|
Debug.Assert(_initialized);
|
||||||
{
|
|
||||||
if (pointer.ToInt64() < region.Pointer.ToInt64() ||
|
|
||||||
pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
|
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64());
|
||||||
|
|
||||||
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
|
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
|
||||||
{
|
{
|
||||||
_cacheAllocators[_activeRegionIndex].Free(funcOffset, AlignCodeSize(entry.Size));
|
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
|
||||||
_cacheEntries.RemoveAt(entryIndex);
|
_cacheEntries.RemoveAt(entryIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ReprotectAsWritable(ReservedRegion region, int offset, int size)
|
private static void ReprotectAsWritable(int offset, int size)
|
||||||
{
|
{
|
||||||
int endOffs = offset + size;
|
int endOffs = offset + size;
|
||||||
|
|
||||||
int regionStart = offset & ~_pageMask;
|
int regionStart = offset & ~_pageMask;
|
||||||
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
|
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
|
||||||
|
|
||||||
region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
_jitRegion.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size)
|
private static void ReprotectAsExecutable(int offset, int size)
|
||||||
{
|
{
|
||||||
int endOffs = offset + size;
|
int endOffs = offset + size;
|
||||||
|
|
||||||
int regionStart = offset & ~_pageMask;
|
int regionStart = offset & ~_pageMask;
|
||||||
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
|
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
|
||||||
|
|
||||||
region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
_jitRegion.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int Allocate(int codeSize, bool deferProtect = false)
|
private static int Allocate(int codeSize, bool deferProtect = false)
|
||||||
@ -220,35 +187,20 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
alignment = 0x4000;
|
alignment = 0x4000;
|
||||||
}
|
}
|
||||||
|
|
||||||
int allocOffset = _cacheAllocators[_activeRegionIndex].Allocate(ref codeSize, alignment);
|
int allocOffset = _cacheAllocator.Allocate(ref codeSize, alignment);
|
||||||
|
|
||||||
if (allocOffset >= 0)
|
Console.WriteLine($"{allocOffset:x8}: {codeSize:x8} {alignment:x8}");
|
||||||
|
|
||||||
|
if (allocOffset < 0)
|
||||||
{
|
{
|
||||||
_jitRegions[_activeRegionIndex].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
throw new OutOfMemoryException("JIT Cache exhausted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
||||||
|
|
||||||
return allocOffset;
|
return allocOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
int exhaustedRegion = _activeRegionIndex;
|
|
||||||
var newRegion = new ReservedRegion(_jitRegions[0].Allocator, CacheSize);
|
|
||||||
_jitRegions.Add(newRegion);
|
|
||||||
_activeRegionIndex = _jitRegions.Count - 1;
|
|
||||||
|
|
||||||
int newRegionNumber = _activeRegionIndex;
|
|
||||||
|
|
||||||
Logger.Info?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {_activeRegionIndex} ({((long)(_activeRegionIndex + 1) * CacheSize)} Total Allocation).");
|
|
||||||
|
|
||||||
_cacheAllocators.Add(new CacheMemoryAllocator(CacheSize));
|
|
||||||
|
|
||||||
int allocOffsetNew = _cacheAllocators[_activeRegionIndex].Allocate(ref codeSize, alignment);
|
|
||||||
if (allocOffsetNew < 0)
|
|
||||||
{
|
|
||||||
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
|
|
||||||
}
|
|
||||||
|
|
||||||
newRegion.ExpandIfNeeded((ulong)allocOffsetNew + (ulong)codeSize);
|
|
||||||
return allocOffsetNew;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int AlignCodeSize(int codeSize, bool deferProtect = false)
|
private static int AlignCodeSize(int codeSize, bool deferProtect = false)
|
||||||
{
|
{
|
||||||
int alignment = CodeAlignment;
|
int alignment = CodeAlignment;
|
||||||
|
@ -28,10 +28,16 @@
|
|||||||
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
|
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
|
||||||
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; };
|
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; };
|
||||||
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
|
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
|
||||||
D1C0A55D2DBFAAD3005AB251 /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = D1C0A55C2DBFAAD3005AB251 /* SwiftUIIntrospect */; };
|
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
4E2953AB2D803BC9000497CD /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = BD43C6212D1B248D003BBC42;
|
||||||
|
remoteInfo = com.Stossy11.MeloNX.RyujinxAg;
|
||||||
|
};
|
||||||
4E80A99E2CD6F54700029585 /* PBXContainerItemProxy */ = {
|
4E80A99E2CD6F54700029585 /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
|
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
|
||||||
@ -192,7 +198,6 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
|
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
|
||||||
D1C0A55D2DBFAAD3005AB251 /* SwiftUIIntrospect in Frameworks */,
|
|
||||||
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
|
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
|
||||||
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */,
|
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
@ -282,6 +287,7 @@
|
|||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
|
4E2953AC2D803BC9000497CD /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
fileSystemSynchronizedGroups = (
|
fileSystemSynchronizedGroups = (
|
||||||
4E80A98F2CD6F54500029585 /* MeloNX */,
|
4E80A98F2CD6F54500029585 /* MeloNX */,
|
||||||
@ -289,7 +295,6 @@
|
|||||||
name = MeloNX;
|
name = MeloNX;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
4EA5AE812D16807500AD0B9F /* SwiftSVG */,
|
4EA5AE812D16807500AD0B9F /* SwiftSVG */,
|
||||||
D1C0A55C2DBFAAD3005AB251 /* SwiftUIIntrospect */,
|
|
||||||
);
|
);
|
||||||
productName = MeloNX;
|
productName = MeloNX;
|
||||||
productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */;
|
productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */;
|
||||||
@ -381,7 +386,6 @@
|
|||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */,
|
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */,
|
||||||
D1C0A55B2DBFAAD3005AB251 /* XCRemoteSwiftPackageReference "swiftui-introspect" */,
|
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 56;
|
preferredProjectObjectVersion = 56;
|
||||||
productRefGroup = 4E80A98E2CD6F54500029585 /* Products */;
|
productRefGroup = 4E80A98E2CD6F54500029585 /* Products */;
|
||||||
@ -469,6 +473,12 @@
|
|||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
/* Begin PBXTargetDependency section */
|
||||||
|
4E2953AC2D803BC9000497CD /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
platformFilter = ios;
|
||||||
|
target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
|
||||||
|
targetProxy = 4E2953AB2D803BC9000497CD /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
4E80A99F2CD6F54700029585 /* PBXTargetDependency */ = {
|
4E80A99F2CD6F54700029585 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = 4E80A98C2CD6F54500029585 /* MeloNX */;
|
target = 4E80A98C2CD6F54500029585 /* MeloNX */;
|
||||||
@ -718,8 +728,6 @@
|
|||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
);
|
);
|
||||||
GCC_OPTIMIZATION_LEVEL = z;
|
GCC_OPTIMIZATION_LEVEL = z;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -902,10 +910,6 @@
|
|||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = "$(VERSION)";
|
MARKETING_VERSION = "$(VERSION)";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||||
@ -1014,8 +1018,6 @@
|
|||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
);
|
);
|
||||||
GCC_OPTIMIZATION_LEVEL = z;
|
GCC_OPTIMIZATION_LEVEL = z;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -1198,10 +1200,6 @@
|
|||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = "$(VERSION)";
|
MARKETING_VERSION = "$(VERSION)";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||||
@ -1411,14 +1409,6 @@
|
|||||||
kind = branch;
|
kind = branch;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
D1C0A55B2DBFAAD3005AB251 /* XCRemoteSwiftPackageReference "swiftui-introspect" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/siteline/swiftui-introspect.git";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMajorVersion;
|
|
||||||
minimumVersion = 1.3.0;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
@ -1427,11 +1417,6 @@
|
|||||||
package = 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */;
|
package = 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */;
|
||||||
productName = SwiftSVG;
|
productName = SwiftSVG;
|
||||||
};
|
};
|
||||||
D1C0A55C2DBFAAD3005AB251 /* SwiftUIIntrospect */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = D1C0A55B2DBFAAD3005AB251 /* XCRemoteSwiftPackageReference "swiftui-introspect" */;
|
|
||||||
productName = SwiftUIIntrospect;
|
|
||||||
};
|
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 4E80A9852CD6F54500029585 /* Project object */;
|
rootObject = 4E80A9852CD6F54500029585 /* Project object */;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "9fd9e5cf42fe0cb11d840e36abe7fbfb590073df6eb786652581b3f6b11d599f",
|
"originHash" : "fedf09a893a63378a2e53f631cd833ae83a0c9ee7338eb8d153b04fd34aaf805",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "swiftsvg",
|
"identity" : "swiftsvg",
|
||||||
@ -9,15 +9,6 @@
|
|||||||
"branch" : "master",
|
"branch" : "master",
|
||||||
"revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d"
|
"revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swiftui-introspect",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/siteline/swiftui-introspect.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336",
|
|
||||||
"version" : "1.3.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 3
|
"version" : 3
|
||||||
|
Binary file not shown.
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<array/>
|
||||||
|
</plist>
|
Binary file not shown.
Binary file not shown.
BIN
src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/ls.xcuserdatad/UserInterfaceState.xcuserstate
generated
Normal file
BIN
src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/ls.xcuserdatad/UserInterfaceState.xcuserstate
generated
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -50,9 +50,7 @@ char* installed_firmware_version();
|
|||||||
|
|
||||||
void set_native_window(void *layerPtr);
|
void set_native_window(void *layerPtr);
|
||||||
|
|
||||||
void pause_emulation(bool shouldPause);
|
void stop_emulation(bool shouldPause);
|
||||||
|
|
||||||
void stop_emulation();
|
|
||||||
|
|
||||||
void initialize();
|
void initialize();
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ struct iOSNav<Content: View>: View {
|
|||||||
|
|
||||||
|
|
||||||
class Ryujinx : ObservableObject {
|
class Ryujinx : ObservableObject {
|
||||||
@Published var isRunning = false
|
private var isRunning = false
|
||||||
|
|
||||||
let virtualController = VirtualController()
|
let virtualController = VirtualController()
|
||||||
|
|
||||||
@ -147,22 +147,6 @@ class Ryujinx : ObservableObject {
|
|||||||
self.games = loadGames()
|
self.games = loadGames()
|
||||||
}
|
}
|
||||||
|
|
||||||
func runloop(_ cool: @escaping () -> Void) {
|
|
||||||
if UserDefaults.standard.bool(forKey: "runOnMainThread") {
|
|
||||||
RunLoop.main.perform {
|
|
||||||
cool()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
thread = Thread {
|
|
||||||
cool()
|
|
||||||
}
|
|
||||||
|
|
||||||
thread.qualityOfService = .userInteractive
|
|
||||||
thread.name = "MeloNX"
|
|
||||||
thread.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct Configuration : Codable, Equatable {
|
public struct Configuration : Codable, Equatable {
|
||||||
var gamepath: String
|
var gamepath: String
|
||||||
var inputids: [String]
|
var inputids: [String]
|
||||||
@ -254,7 +238,7 @@ class Ryujinx : ObservableObject {
|
|||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
|
|
||||||
runloop { [self] in
|
thread = Thread { [self] in
|
||||||
|
|
||||||
isRunning = true
|
isRunning = true
|
||||||
|
|
||||||
@ -315,6 +299,10 @@ class Ryujinx : ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
thread.qualityOfService = .userInteractive
|
||||||
|
thread.name = "MeloNX"
|
||||||
|
thread.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveArrayAsTextFile(strings: [String], filePath: String) {
|
func saveArrayAsTextFile(strings: [String], filePath: String) {
|
||||||
@ -386,6 +374,10 @@ class Ryujinx : ObservableObject {
|
|||||||
thread.cancel()
|
thread.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var running: Bool {
|
||||||
|
return isRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func loadGames() -> [Game] {
|
func loadGames() -> [Game] {
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
@ -476,7 +468,6 @@ class Ryujinx : ObservableObject {
|
|||||||
|
|
||||||
args.append(contentsOf: ["--aspect-ratio", config.aspectRatio.rawValue])
|
args.append(contentsOf: ["--aspect-ratio", config.aspectRatio.rawValue])
|
||||||
|
|
||||||
|
|
||||||
if config.nintendoinput {
|
if config.nintendoinput {
|
||||||
args.append("--correct-controller")
|
args.append("--correct-controller")
|
||||||
}
|
}
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
//
|
|
||||||
// ToggleButtonsState.swift
|
|
||||||
// MeloNX
|
|
||||||
//
|
|
||||||
// Created by Stossy11 on 12/04/2025.
|
|
||||||
//
|
|
||||||
|
|
||||||
|
|
||||||
struct ToggleButtonsState: Codable, Equatable {
|
|
||||||
var toggle1: Bool
|
|
||||||
var toggle2: Bool
|
|
||||||
var toggle3: Bool
|
|
||||||
var toggle4: Bool
|
|
||||||
|
|
||||||
init() {
|
|
||||||
self = .default
|
|
||||||
}
|
|
||||||
|
|
||||||
init(toggle1: Bool, toggle2: Bool, toggle3: Bool, toggle4: Bool) {
|
|
||||||
self.toggle1 = toggle1
|
|
||||||
self.toggle2 = toggle2
|
|
||||||
self.toggle3 = toggle3
|
|
||||||
self.toggle4 = toggle4
|
|
||||||
}
|
|
||||||
|
|
||||||
static let `default` = ToggleButtonsState(toggle1: false, toggle2: false, toggle3: false, toggle4: false)
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
//
|
|
||||||
// AppCodableStorage.swift
|
|
||||||
// MeloNX
|
|
||||||
//
|
|
||||||
// Created by Stossy11 on 12/04/2025.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
@propertyWrapper
|
|
||||||
struct AppCodableStorage<Value: Codable & Equatable>: 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<Value> {
|
|
||||||
Binding(
|
|
||||||
get: { self.wrappedValue },
|
|
||||||
set: { newValue in self.wrappedValue = newValue }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
//
|
|
||||||
// NavigationItemPalette.swift
|
|
||||||
// iTorrent
|
|
||||||
//
|
|
||||||
// Created by Daniil Vinogradov on 14.11.2024.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import SwiftUI
|
|
||||||
import SwiftUIIntrospect
|
|
||||||
|
|
||||||
public extension View {
|
|
||||||
func navitaionItemBottomPalette(@ViewBuilder body: () -> (some View)) -> some View {
|
|
||||||
modifier(NavitaionItemBottomPaletteContent(content: body().asController))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NavitaionItemBottomPaletteContent: ViewModifier {
|
|
||||||
let content: UIViewController
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
content
|
|
||||||
.introspect(.viewController, on: .iOS(.v14, .v15, .v16, .v17, .v18), customize: { viewController in
|
|
||||||
let view = self.content.view!
|
|
||||||
view.backgroundColor = .clear
|
|
||||||
let size = view.systemLayoutSizeFitting(.init(width: viewController.view.frame.width, height: UIView.layoutFittingCompressedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
|
|
||||||
viewController.navigationItem.setBottomPalette(view, height: size.height)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension UINavigationItem {
|
|
||||||
func setBottomPalette(_ contentView: UIView?, height: CGFloat = 44) {
|
|
||||||
/// "_setBottomPalette:"
|
|
||||||
let selector = NSSelectorFromBase64String("X3NldEJvdHRvbVBhbGV0dGU6")
|
|
||||||
guard responds(to: selector) else { return }
|
|
||||||
perform(selector, with: Self.makeNavigationItemPalette(with: contentView, height: height))
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func makeNavigationItemPalette(with contentView: UIView?, height: CGFloat) -> UIView? {
|
|
||||||
guard let contentView else { return nil }
|
|
||||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
|
|
||||||
let contentViewHolder = UIView(frame: .init(x: 0, y: 0, width: 0, height: height))
|
|
||||||
contentViewHolder.autoresizingMask = [.flexibleHeight]
|
|
||||||
contentViewHolder.addSubview(contentView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
contentViewHolder.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
|
||||||
contentViewHolder.topAnchor.constraint(equalTo: contentView.topAnchor),
|
|
||||||
contentView.trailingAnchor.constraint(equalTo: contentViewHolder.trailingAnchor),
|
|
||||||
contentView.bottomAnchor.constraint(equalTo: contentViewHolder.bottomAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
/// "_UINavigationBarPalette"
|
|
||||||
guard let paletteClass = NSClassFromBase64String("X1VJTmF2aWdhdGlvbkJhclBhbGV0dGU=") as? UIView.Type
|
|
||||||
else { return nil }
|
|
||||||
|
|
||||||
/// "alloc"
|
|
||||||
/// "initWithContentView:"
|
|
||||||
guard let palette = paletteClass.perform(NSSelectorFromBase64String("YWxsb2M="))
|
|
||||||
.takeUnretainedValue()
|
|
||||||
.perform(NSSelectorFromBase64String("aW5pdFdpdGhDb250ZW50Vmlldzo="), with: contentViewHolder)
|
|
||||||
.takeUnretainedValue() as? UIView
|
|
||||||
else { return nil }
|
|
||||||
|
|
||||||
palette.preservesSuperviewLayoutMargins = true
|
|
||||||
return palette
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NSSelectorFromBase64String(_ base64String: String) -> Selector {
|
|
||||||
NSSelectorFromString(String(base64: base64String))
|
|
||||||
}
|
|
||||||
|
|
||||||
func NSClassFromBase64String(_ aBase64ClassName: String) -> AnyClass? {
|
|
||||||
NSClassFromString(String(base64: aBase64ClassName))
|
|
||||||
}
|
|
||||||
|
|
||||||
extension String {
|
|
||||||
init(base64: String) {
|
|
||||||
self.init(data: Data(base64Encoded: base64)!, encoding: .utf8)!
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
//
|
|
||||||
// UIKitSwiftUIInarop.swift
|
|
||||||
// iTorrent
|
|
||||||
//
|
|
||||||
// Created by Daniil Vinogradov on 01/11/2023.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
private struct GenericControllerView: UIViewControllerRepresentable {
|
|
||||||
let viewController: UIViewController
|
|
||||||
typealias UIViewControllerType = UIViewController
|
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> UIViewController {
|
|
||||||
viewController
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIViewController(_ uiViewController: UIViewController, context: Context) { /* Ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension View {
|
|
||||||
@MainActor
|
|
||||||
var asController: UIHostingController<Self> {
|
|
||||||
let vc = UIHostingController<Self>(rootView: self)
|
|
||||||
if #available(iOS 16.4, *) {
|
|
||||||
vc.safeAreaRegions = []
|
|
||||||
}
|
|
||||||
return vc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension UIViewController {
|
|
||||||
var asView: some View {
|
|
||||||
GenericControllerView(viewController: self)
|
|
||||||
}
|
|
||||||
}
|
|
@ -45,7 +45,7 @@ struct ContentView: View {
|
|||||||
@AppStorage("quit") var quit: Bool = false
|
@AppStorage("quit") var quit: Bool = false
|
||||||
@State var quits: Bool = false
|
@State var quits: Bool = false
|
||||||
@AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = true
|
@AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = true
|
||||||
@AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = false
|
@AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = true
|
||||||
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false
|
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false
|
||||||
|
|
||||||
// Loading Animation
|
// Loading Animation
|
||||||
@ -302,6 +302,8 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func setupEmulation() {
|
private func setupEmulation() {
|
||||||
|
refreshControllersList()
|
||||||
|
|
||||||
isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil)
|
isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
@ -319,6 +321,25 @@ struct ContentView: View {
|
|||||||
controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) })
|
controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) })
|
||||||
controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") }
|
controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") }
|
||||||
|
|
||||||
|
|
||||||
|
if !currentControllers.isEmpty, !(currentControllers.count == 1) {
|
||||||
|
var currentController: [Controller] = []
|
||||||
|
|
||||||
|
if currentController.count == 1 {
|
||||||
|
currentController.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 }) {
|
||||||
|
currentController.append(controller)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentController == currentControllers {
|
||||||
|
currentControllers = []
|
||||||
|
currentControllers = currentController
|
||||||
|
}
|
||||||
|
} else {
|
||||||
currentControllers = []
|
currentControllers = []
|
||||||
|
|
||||||
if controllersList.count == 1 {
|
if controllersList.count == 1 {
|
||||||
@ -331,6 +352,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func start(displayid: UInt32) {
|
private func start(displayid: UInt32) {
|
||||||
guard let game else { return }
|
guard let game else { return }
|
||||||
@ -390,7 +412,6 @@ struct ContentView: View {
|
|||||||
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||||
components.host == "game" {
|
components.host == "game" {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
refreshControllersList()
|
|
||||||
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
|
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
|
||||||
game = ryujinx.games.first(where: { $0.titleId == text })
|
game = ryujinx.games.first(where: { $0.titleId == text })
|
||||||
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
|
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
|
@ -70,11 +70,11 @@ struct ControllerView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
ButtonView(button: .leftStick)
|
ButtonView(button: .leftStick)
|
||||||
.padding()
|
.padding()
|
||||||
ButtonView(button: .back)
|
ButtonView(button: .start)
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
ButtonView(button: .start)
|
ButtonView(button: .back)
|
||||||
ButtonView(button: .rightStick)
|
ButtonView(button: .rightStick)
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
@ -257,180 +257,148 @@ struct ABXYView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct ButtonView: View {
|
struct ButtonView: View {
|
||||||
var button: VirtualControllerButton
|
var button: VirtualControllerButton
|
||||||
|
@State private var width: CGFloat = 45
|
||||||
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
|
@State private var height: CGFloat = 45
|
||||||
@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 isPressed = false
|
||||||
@State private var toggleState = false
|
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
|
||||||
|
@Environment(\.presentationMode) var presentationMode
|
||||||
@State private var size: CGSize = .zero
|
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||||
|
@State private var debounceTimer: Timer?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Circle()
|
Image(systemName: buttonText)
|
||||||
.foregroundStyle(.clear.opacity(0))
|
|
||||||
.overlay {
|
|
||||||
Image(systemName: buttonConfig.iconName)
|
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
.frame(width: size.width, height: size.height)
|
.frame(width: width, height: height)
|
||||||
.foregroundStyle(.white)
|
.foregroundColor(true ? Color.white.opacity(0.5) : Color.black.opacity(0.5))
|
||||||
.opacity(isPressed ? 0.6 : 1.0)
|
|
||||||
.allowsHitTesting(false)
|
|
||||||
}
|
|
||||||
.frame(width: size.width, height: size.height)
|
|
||||||
.background(
|
.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 {
|
Group {
|
||||||
if !button.isTrigger && button != .leftStick && button != .rightStick {
|
if !button.isTrigger && button != .leftStick && button != .rightStick {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(Color.gray.opacity(0.4))
|
.fill(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3))
|
||||||
.frame(width: size.width * 1.25, height: size.height * 1.25)
|
.frame(width: width * 1.25, height: height * 1.25)
|
||||||
} else if button == .leftStick || button == .rightStick {
|
} else if button == .leftStick || button == .rightStick {
|
||||||
Image(systemName: buttonConfig.iconName)
|
Image(systemName: buttonText)
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
.frame(width: size.width * 1.25, height: size.height * 1.25)
|
.frame(width: width * 1.25, height: height * 1.25)
|
||||||
.foregroundColor(Color.gray.opacity(0.4))
|
.foregroundColor(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3))
|
||||||
} else if button.isTrigger {
|
} else if button.isTrigger {
|
||||||
Image(systemName: convertTriggerIconToButton(buttonConfig.iconName))
|
Image(systemName: "" + String(turntobutton(buttonText)))
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
.frame(width: size.width * 1.25, height: size.height * 1.25)
|
.frame(width: width * 1.25, height: height * 1.25)
|
||||||
.foregroundColor(Color.gray.opacity(0.4))
|
.foregroundColor(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
.opacity(isPressed ? 0.6 : 1.0)
|
||||||
|
.gesture(
|
||||||
|
DragGesture(minimumDistance: 0)
|
||||||
|
.onChanged { _ in
|
||||||
|
handleButtonPress()
|
||||||
|
}
|
||||||
|
.onEnded { _ in
|
||||||
|
handleButtonRelease()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.onAppear {
|
||||||
|
configureSizeForButton()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func convertTriggerIconToButton(_ iconName: String) -> String {
|
private func turntobutton(_ string: String) -> String {
|
||||||
if iconName.hasPrefix("zl") || iconName.hasPrefix("zr") {
|
var sting = string
|
||||||
var converted = String(iconName.dropFirst(3))
|
if string.hasPrefix("zl") || string.hasPrefix("zr") {
|
||||||
converted = converted.replacingOccurrences(of: "rectangle", with: "button")
|
sting = String(string.dropFirst(3))
|
||||||
converted = converted.replacingOccurrences(of: ".fill", with: ".horizontal.fill")
|
|
||||||
return converted
|
|
||||||
} else {
|
} else {
|
||||||
var converted = String(iconName.dropFirst(2))
|
sting = String(string.dropFirst(2))
|
||||||
converted = converted.replacingOccurrences(of: "rectangle", with: "button")
|
|
||||||
converted = converted.replacingOccurrences(of: ".fill", with: ".horizontal.fill")
|
|
||||||
return converted
|
|
||||||
}
|
}
|
||||||
|
sting = sting.replacingOccurrences(of: "rectangle", with: "button")
|
||||||
|
sting = sting.replacingOccurrences(of: ".fill", with: ".horizontal.fill")
|
||||||
|
|
||||||
|
return sting
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleButtonPress() {
|
private func handleButtonPress() {
|
||||||
guard !isPressed || istoggle else { return }
|
if !isPressed {
|
||||||
|
|
||||||
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
|
isPressed = true
|
||||||
|
|
||||||
|
debounceTimer?.invalidate()
|
||||||
|
|
||||||
Ryujinx.shared.virtualController.setButtonState(1, for: button)
|
Ryujinx.shared.virtualController.setButtonState(1, for: button)
|
||||||
|
|
||||||
Haptics.shared.play(.medium)
|
Haptics.shared.play(.medium)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleButtonRelease() {
|
private func handleButtonRelease() {
|
||||||
if istoggle { return }
|
if isPressed {
|
||||||
|
|
||||||
guard isPressed else { return }
|
|
||||||
|
|
||||||
isPressed = false
|
isPressed = false
|
||||||
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.05) {
|
|
||||||
|
debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: false) { _ in
|
||||||
Ryujinx.shared.virtualController.setButtonState(0, for: button)
|
Ryujinx.shared.virtualController.setButtonState(0, for: button)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func calculateButtonSize() -> CGSize {
|
private func configureSizeForButton() {
|
||||||
let baseWidth: CGFloat
|
|
||||||
let baseHeight: CGFloat
|
|
||||||
|
|
||||||
if button.isTrigger {
|
if button.isTrigger {
|
||||||
baseWidth = 70
|
width = 70
|
||||||
baseHeight = 40
|
height = 40
|
||||||
} else if button.isSmall {
|
} else if button.isSmall {
|
||||||
baseWidth = 35
|
width = 35
|
||||||
baseHeight = 35
|
height = 35
|
||||||
} else {
|
|
||||||
baseWidth = 45
|
|
||||||
baseHeight = 45
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let deviceMultiplier = UIDevice.current.userInterfaceIdiom == .pad ? 1.2 : 1.0
|
// Adjust for iPad
|
||||||
let scaleMultiplier = CGFloat(controllerScale)
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
|
width *= 1.2
|
||||||
return CGSize(
|
height *= 1.2
|
||||||
width: baseWidth * deviceMultiplier * scaleMultiplier,
|
|
||||||
height: baseHeight * deviceMultiplier * scaleMultiplier
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Centralized button configuration
|
width *= CGFloat(controllerScale)
|
||||||
private var buttonConfig: ButtonConfiguration {
|
height *= CGFloat(controllerScale)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var buttonText: String {
|
||||||
switch button {
|
switch button {
|
||||||
case .A:
|
case .A:
|
||||||
return ButtonConfiguration(iconName: "a.circle.fill")
|
return "a.circle.fill"
|
||||||
case .B:
|
case .B:
|
||||||
return ButtonConfiguration(iconName: "b.circle.fill")
|
return "b.circle.fill"
|
||||||
case .X:
|
case .X:
|
||||||
return ButtonConfiguration(iconName: "x.circle.fill")
|
return "x.circle.fill"
|
||||||
case .Y:
|
case .Y:
|
||||||
return ButtonConfiguration(iconName: "y.circle.fill")
|
return "y.circle.fill"
|
||||||
case .leftStick:
|
case .leftStick:
|
||||||
return ButtonConfiguration(iconName: "l.joystick.press.down.fill")
|
return "l.joystick.press.down.fill"
|
||||||
case .rightStick:
|
case .rightStick:
|
||||||
return ButtonConfiguration(iconName: "r.joystick.press.down.fill")
|
return "r.joystick.press.down.fill"
|
||||||
case .dPadUp:
|
case .dPadUp:
|
||||||
return ButtonConfiguration(iconName: "arrowtriangle.up.circle.fill")
|
return "arrowtriangle.up.circle.fill"
|
||||||
case .dPadDown:
|
case .dPadDown:
|
||||||
return ButtonConfiguration(iconName: "arrowtriangle.down.circle.fill")
|
return "arrowtriangle.down.circle.fill"
|
||||||
case .dPadLeft:
|
case .dPadLeft:
|
||||||
return ButtonConfiguration(iconName: "arrowtriangle.left.circle.fill")
|
return "arrowtriangle.left.circle.fill"
|
||||||
case .dPadRight:
|
case .dPadRight:
|
||||||
return ButtonConfiguration(iconName: "arrowtriangle.right.circle.fill")
|
return "arrowtriangle.right.circle.fill"
|
||||||
case .leftTrigger:
|
case .leftTrigger:
|
||||||
return ButtonConfiguration(iconName: "zl.rectangle.roundedtop.fill")
|
return "zl.rectangle.roundedtop.fill"
|
||||||
case .rightTrigger:
|
case .rightTrigger:
|
||||||
return ButtonConfiguration(iconName: "zr.rectangle.roundedtop.fill")
|
return "zr.rectangle.roundedtop.fill"
|
||||||
case .leftShoulder:
|
case .leftShoulder:
|
||||||
return ButtonConfiguration(iconName: "l.rectangle.roundedbottom.fill")
|
return "l.rectangle.roundedbottom.fill"
|
||||||
case .rightShoulder:
|
case .rightShoulder:
|
||||||
return ButtonConfiguration(iconName: "r.rectangle.roundedbottom.fill")
|
return "r.rectangle.roundedbottom.fill"
|
||||||
case .start:
|
case .start:
|
||||||
return ButtonConfiguration(iconName: "plus.circle.fill")
|
return "plus.circle.fill"
|
||||||
case .back:
|
case .back:
|
||||||
return ButtonConfiguration(iconName: "minus.circle.fill")
|
return "minus.circle.fill"
|
||||||
case .guide:
|
case .guide:
|
||||||
return ButtonConfiguration(iconName: "house.circle.fill")
|
return "house.circle.fill"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ButtonConfiguration {
|
|
||||||
let iconName: String
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,125 +0,0 @@
|
|||||||
//
|
|
||||||
// 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())
|
|
||||||
}
|
|
||||||
}
|
|
@ -65,26 +65,27 @@ struct EmulationView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
if ssb {
|
if ssb {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
||||||
Image(systemName: "arrow.left.circle")
|
Button {
|
||||||
.resizable()
|
if let screenshot = Ryujinx.shared.emulationUIView?.screenshot() {
|
||||||
.frame(width: 50, height: 50)
|
UIImageWriteToSavedPhotosAlbum(screenshot, nil, nil, nil)
|
||||||
.onTapGesture {
|
|
||||||
startgame = nil
|
|
||||||
stop_emulation()
|
|
||||||
try? Ryujinx.shared.stop()
|
|
||||||
}
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "square.and.arrow.up")
|
||||||
|
}
|
||||||
|
.frame(width: UIDevice.current.systemName.contains("iPadOS") ? 60 * 1.2 : 45, height: UIDevice.current.systemName.contains("iPadOS") ? 60 * 1.2 : 45)
|
||||||
.padding()
|
.padding()
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -101,13 +102,13 @@ struct EmulationView: View {
|
|||||||
.onChange(of: scenePhase) { newPhase in
|
.onChange(of: scenePhase) { newPhase in
|
||||||
// Detect when the app enters the background
|
// Detect when the app enters the background
|
||||||
if newPhase == .background {
|
if newPhase == .background {
|
||||||
pause_emulation(true)
|
stop_emulation(true)
|
||||||
isInBackground = true
|
isInBackground = true
|
||||||
} else if newPhase == .active {
|
} else if newPhase == .active {
|
||||||
pause_emulation(false)
|
stop_emulation(false)
|
||||||
isInBackground = false
|
isInBackground = false
|
||||||
} else if newPhase == .inactive {
|
} else if newPhase == .inactive {
|
||||||
pause_emulation(true)
|
stop_emulation(true)
|
||||||
isInBackground = true
|
isInBackground = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,11 +87,8 @@ class MeloMTKView: MTKView {
|
|||||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
super.touchesBegan(touches, with: event)
|
super.touchesBegan(touches, with: event)
|
||||||
|
|
||||||
let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
|
|
||||||
|
|
||||||
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
|
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
|
||||||
|
|
||||||
if !disabled {
|
|
||||||
for touch in touches {
|
for touch in touches {
|
||||||
let location = touch.location(in: self)
|
let location = touch.location(in: self)
|
||||||
if scaleToTargetResolution(location) == nil {
|
if scaleToTargetResolution(location) == nil {
|
||||||
@ -107,16 +104,12 @@ class MeloMTKView: MTKView {
|
|||||||
touch_began(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index))
|
touch_began(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
super.touchesEnded(touches, with: event)
|
super.touchesEnded(touches, with: event)
|
||||||
|
|
||||||
let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
|
|
||||||
|
|
||||||
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
|
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
|
||||||
|
|
||||||
if !disabled {
|
|
||||||
for touch in touches {
|
for touch in touches {
|
||||||
if ignoredTouches.contains(touch) {
|
if ignoredTouches.contains(touch) {
|
||||||
ignoredTouches.remove(touch)
|
ignoredTouches.remove(touch)
|
||||||
@ -131,16 +124,12 @@ class MeloMTKView: MTKView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
super.touchesMoved(touches, with: event)
|
super.touchesMoved(touches, with: event)
|
||||||
|
|
||||||
let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
|
|
||||||
|
|
||||||
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
|
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
|
||||||
|
|
||||||
if !disabled {
|
|
||||||
for touch in touches {
|
for touch in touches {
|
||||||
if ignoredTouches.contains(touch) {
|
if ignoredTouches.contains(touch) {
|
||||||
continue
|
continue
|
||||||
@ -162,5 +151,4 @@ class MeloMTKView: MTKView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -96,8 +96,8 @@ struct GameInfoSheet: View {
|
|||||||
.navigationTitle(game.titleName)
|
.navigationTitle(game.titleName)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button("Dismiss") {
|
Button("Done") {
|
||||||
presentationMode.wrappedValue.dismiss()
|
presentationMode.wrappedValue.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -61,16 +61,12 @@ struct GameLibraryView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
iOSNav {
|
iOSNav {
|
||||||
|
ZStack {
|
||||||
|
// Background color
|
||||||
|
Color(UIColor.systemBackground)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// Game list
|
|
||||||
if Ryujinx.shared.games.isEmpty {
|
|
||||||
EmptyGameLibraryView(isSelectingGameFile: $isSelectingGameFile)
|
|
||||||
} else {
|
|
||||||
gameListView
|
|
||||||
.animation(.easeInOut(duration: 0.3), value: searchText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navitaionItemBottomPalette {
|
|
||||||
// Header with stats
|
// Header with stats
|
||||||
if !Ryujinx.shared.games.isEmpty {
|
if !Ryujinx.shared.games.isEmpty {
|
||||||
GameLibraryHeader(
|
GameLibraryHeader(
|
||||||
@ -78,23 +74,19 @@ struct GameLibraryView: View {
|
|||||||
recentGames: realRecentGames.count,
|
recentGames: realRecentGames.count,
|
||||||
firmwareVersion: firmwareversion
|
firmwareVersion: firmwareversion
|
||||||
)
|
)
|
||||||
.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()
|
|
||||||
|
// Game list
|
||||||
|
if Ryujinx.shared.games.isEmpty {
|
||||||
|
EmptyGameLibraryView(
|
||||||
|
isSelectingGameFile: $isSelectingGameFile,
|
||||||
|
isImporting: $isImporting
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
gameListView
|
||||||
|
.animation(.easeInOut(duration: 0.3), value: searchText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.navigationTitle("Game Library")
|
.navigationTitle("Game Library")
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
@ -158,38 +150,36 @@ struct GameLibraryView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.overlay(Group {
|
||||||
|
if ryujinx.jitenabled {
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Circle()
|
||||||
|
.frame(width: 12, height: 12)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.foregroundColor(Color.green)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.onChange(of: startemu) { game in
|
.onChange(of: startemu) { game in
|
||||||
guard let game else { return }
|
guard let game else { return }
|
||||||
addToRecentGames(game)
|
addToRecentGames(game)
|
||||||
}
|
}
|
||||||
.searchable(text: $searchText, placement: .toolbar, prompt: "Search games or developers")
|
// .searchable(text: $searchText, placement: .toolbar, prompt: "Search games or developers")
|
||||||
.onChange(of: searchText) { _ in
|
.onChange(of: searchText) { _ in
|
||||||
isSearching = !searchText.isEmpty
|
isSearching = !searchText.isEmpty
|
||||||
}
|
}
|
||||||
.onChange(of: isImporting) { newValue in
|
.fileImporter(isPresented: $isImporting, allowedContentTypes: [.folder, .nsp, .xci, .zip, .item]) { result in
|
||||||
if newValue {
|
handleFileImport(result: result)
|
||||||
FileImporterManager.shared.importFiles(types: [.nsp, .xci, .item]) { result in
|
|
||||||
isImporting = false
|
|
||||||
handleRunningGame(result: result)
|
|
||||||
}
|
}
|
||||||
}
|
.fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in
|
||||||
}
|
|
||||||
.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)
|
handleFirmwareImport(result: result)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $isSelectingGameUpdate) {
|
.sheet(isPresented: $isSelectingGameUpdate) {
|
||||||
UpdateManagerSheet(game: $gameInfo)
|
UpdateManagerSheet(game: $gameInfo)
|
||||||
}
|
}
|
||||||
@ -249,7 +239,6 @@ struct GameLibraryView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Library Section
|
// Library Section
|
||||||
if !filteredGames.isEmpty {
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text("Library")
|
Text("Library")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
@ -272,7 +261,6 @@ struct GameLibraryView: View {
|
|||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
ForEach(filteredGames) { game in
|
ForEach(filteredGames) { game in
|
||||||
GameListRow(
|
GameListRow(
|
||||||
@ -285,7 +273,7 @@ struct GameLibraryView: View {
|
|||||||
gameRequirements: $gameRequirements,
|
gameRequirements: $gameRequirements,
|
||||||
gameInfo: $gameInfo
|
gameInfo: $gameInfo
|
||||||
)
|
)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal, 3)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -371,10 +359,11 @@ struct GameLibraryView: View {
|
|||||||
|
|
||||||
// MARK: - Import Handlers
|
// MARK: - Import Handlers
|
||||||
|
|
||||||
private func handleAddingGame(result: Result<[URL], Error>) {
|
private func handleFileImport(result: Result<URL, Error>) {
|
||||||
|
if isSelectingGameFile {
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let urls):
|
case .success(let url):
|
||||||
guard let url = urls.first, url.startAccessingSecurityScopedResource() else {
|
guard url.startAccessingSecurityScopedResource() else {
|
||||||
// print("Failed to access security-scoped resource")
|
// print("Failed to access security-scoped resource")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -399,12 +388,10 @@ struct GameLibraryView: View {
|
|||||||
case .failure(let err):
|
case .failure(let err):
|
||||||
print("File import failed: \(err.localizedDescription)")
|
print("File import failed: \(err.localizedDescription)")
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
|
||||||
private func handleRunningGame(result: Result<[URL], Error>) {
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let urls):
|
case .success(let url):
|
||||||
guard let url = urls.first, url.startAccessingSecurityScopedResource() else {
|
guard url.startAccessingSecurityScopedResource() else {
|
||||||
// print("Failed to access security-scoped resource")
|
// print("Failed to access security-scoped resource")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -429,14 +416,11 @@ struct GameLibraryView: View {
|
|||||||
print("File import failed: \(err.localizedDescription)")
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleFirmwareImport(result: Result<URL, Error>) {
|
||||||
|
switch result {
|
||||||
|
case .success(let url):
|
||||||
do {
|
do {
|
||||||
let fun = url.startAccessingSecurityScopedResource()
|
let fun = url.startAccessingSecurityScopedResource()
|
||||||
let path = url.path
|
let path = url.path
|
||||||
@ -541,6 +525,7 @@ extension Game: Codable {
|
|||||||
// MARK: - Empty Library View
|
// MARK: - Empty Library View
|
||||||
struct EmptyGameLibraryView: View {
|
struct EmptyGameLibraryView: View {
|
||||||
@Binding var isSelectingGameFile: Bool
|
@Binding var isSelectingGameFile: Bool
|
||||||
|
@Binding var isImporting: Bool
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 24) {
|
VStack(spacing: 24) {
|
||||||
@ -563,6 +548,7 @@ struct EmptyGameLibraryView: View {
|
|||||||
|
|
||||||
Button {
|
Button {
|
||||||
isSelectingGameFile = true
|
isSelectingGameFile = true
|
||||||
|
isImporting = true
|
||||||
} label: {
|
} label: {
|
||||||
Label("Add Game", systemImage: "plus")
|
Label("Add Game", systemImage: "plus")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
@ -591,7 +577,7 @@ struct GameLibraryHeader: View {
|
|||||||
// Stats cards
|
// Stats cards
|
||||||
StatCard(
|
StatCard(
|
||||||
icon: "gamecontroller.fill",
|
icon: "gamecontroller.fill",
|
||||||
title: "Games",
|
title: "Total Games",
|
||||||
value: "\(totalGames)",
|
value: "\(totalGames)",
|
||||||
color: .blue
|
color: .blue
|
||||||
)
|
)
|
||||||
@ -611,7 +597,8 @@ struct GameLibraryHeader: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.padding(.bottom, 8)
|
.padding(.top, 8)
|
||||||
|
.padding(.bottom, 4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -847,7 +834,7 @@ struct GameListRow: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, 10)
|
.padding(.horizontal, 10)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(width: .infinity, height: .infinity)
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
@ -1140,7 +1127,7 @@ func pullGameCompatibility(completion: @escaping (Result<[GameRequirements], Err
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let url = URL(string: "https://melonx.net/api/game_entries") else {
|
guard let url = URL(string: "https://melonx.org/api/game_entries") else {
|
||||||
completion(.failure(NSError(domain: "Invalid URL", code: 0, userInfo: nil)))
|
completion(.failure(NSError(domain: "Invalid URL", code: 0, userInfo: nil)))
|
||||||
return
|
return
|
||||||
}
|
}
|
@ -56,15 +56,6 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
@AppStorage("HideButtons") var hideButtonsJoy = false
|
@AppStorage("HideButtons") var hideButtonsJoy = false
|
||||||
|
|
||||||
@AppStorage("checkForUpdate") var checkForUpdate: Bool = true
|
|
||||||
|
|
||||||
@AppStorage("disableTouch") var disableTouch = false
|
|
||||||
|
|
||||||
@AppStorage("runOnMainThread") var runOnMainThread = false
|
|
||||||
|
|
||||||
@AppCodableStorage("toggleButtons") var toggleButtons = ToggleButtonsState()
|
|
||||||
|
|
||||||
|
|
||||||
@State private var showResolutionInfo = false
|
@State private var showResolutionInfo = false
|
||||||
@State private var showAnisotropicInfo = false
|
@State private var showAnisotropicInfo = false
|
||||||
@State private var showControllerInfo = false
|
@State private var showControllerInfo = false
|
||||||
@ -102,17 +93,34 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var appVersion: String {
|
|
||||||
guard let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
|
|
||||||
return "Unknown"
|
|
||||||
}
|
|
||||||
return version
|
|
||||||
}
|
|
||||||
|
|
||||||
@FocusState private var isArgumentsKeyboardVisible: Bool
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
iOSNav {
|
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 {
|
ScrollView {
|
||||||
VStack(spacing: 24) {
|
VStack(spacing: 24) {
|
||||||
// Device Info Card
|
// Device Info Card
|
||||||
@ -132,30 +140,13 @@ struct SettingsView: View {
|
|||||||
case .misc:
|
case .misc:
|
||||||
miscSettings
|
miscSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: 50)
|
||||||
}
|
}
|
||||||
.padding(.bottom)
|
.padding(.bottom)
|
||||||
}
|
}
|
||||||
.scrollDismissesKeyboardIfAvailable()
|
|
||||||
.navitaionItemBottomPalette {
|
|
||||||
// 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
|
|
||||||
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.bottom, 8)
|
|
||||||
}
|
|
||||||
.defaultScrollAnchorIsAvailable(.center)
|
|
||||||
}
|
|
||||||
.navigationTitle("Settings")
|
.navigationTitle("Settings")
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
// .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic))
|
// .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic))
|
||||||
@ -198,14 +189,6 @@ struct SettingsView: View {
|
|||||||
Text("\(memoryText) RAM")
|
Text("\(memoryText) RAM")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
Text("·")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Text("Version \(appVersion)")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Device cards
|
// Device cards
|
||||||
@ -477,16 +460,6 @@ struct SettingsView: View {
|
|||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
SettingsToggle(isOn: $swapBandA, icon: "rectangle.2.swap", label: "Swap Face Buttons (Physical Controller)")
|
SettingsToggle(isOn: $swapBandA, icon: "rectangle.2.swap", label: "Swap Face Buttons (Physical Controller)")
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
DisclosureGroup("Toggle Buttons") {
|
|
||||||
SettingsToggle(isOn: $toggleButtons.toggle1, icon: "circle.grid.cross.right.filled", label: "Toggle A")
|
|
||||||
SettingsToggle(isOn: $toggleButtons.toggle2, icon: "circle.grid.cross.down.filled", label: "Toggle B")
|
|
||||||
SettingsToggle(isOn: $toggleButtons.toggle3, icon: "circle.grid.cross.up.filled", label: "Toggle X")
|
|
||||||
SettingsToggle(isOn: $toggleButtons.toggle4, icon: "circle.grid.cross.left.filled", label: "Toggle Y")
|
|
||||||
}
|
|
||||||
.padding(.vertical, 6)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -734,10 +707,6 @@ struct SettingsView: View {
|
|||||||
// Advanced toggles card
|
// Advanced toggles card
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
VStack(spacing: 4) {
|
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")
|
SettingsToggle(isOn: $config.dfsIntegrityChecks, icon: "checkmark.shield", label: "Disable FS Integrity Checks")
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
@ -778,7 +747,7 @@ struct SettingsView: View {
|
|||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
if #available(iOS 15.0, *) {
|
if #available(iOS 15.0, *) {
|
||||||
TextField("Separate arguments with commas" ,text: Binding(
|
TextField("Separate arguments with commas", text: Binding(
|
||||||
get: {
|
get: {
|
||||||
config.additionalArgs.joined(separator: ", ")
|
config.additionalArgs.joined(separator: ", ")
|
||||||
},
|
},
|
||||||
@ -793,14 +762,6 @@ struct SettingsView: View {
|
|||||||
.textInputAutocapitalization(.none)
|
.textInputAutocapitalization(.none)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .keyboard) {
|
|
||||||
Button("Dismiss") {
|
|
||||||
isArgumentsKeyboardVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.focused($isArgumentsKeyboardVisible)
|
|
||||||
} else {
|
} else {
|
||||||
TextField("Separate arguments with commas", text: Binding(
|
TextField("Separate arguments with commas", text: Binding(
|
||||||
get: {
|
get: {
|
||||||
@ -848,13 +809,8 @@ struct SettingsView: View {
|
|||||||
SettingsSection(title: "Miscellaneous Options") {
|
SettingsSection(title: "Miscellaneous Options") {
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
// Disable Touch card
|
|
||||||
SettingsToggle(isOn: $disableTouch, icon: "rectangle.and.hand.point.up.left.filled", label: "Disable Touch")
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
// Screenshot button card
|
// Screenshot button card
|
||||||
SettingsToggle(isOn: $ssb, icon: "arrow.left.circle", label: "Exit Button")
|
SettingsToggle(isOn: $ssb, icon: "square.and.arrow.up", label: "Screenshot Button")
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
@ -905,9 +861,6 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
SettingsToggle(isOn: $checkForUpdate, icon: "square.and.arrow.down", label: "Check for Updates")
|
|
||||||
|
|
||||||
if ryujinx.firmwareversion != "0" {
|
if ryujinx.firmwareversion != "0" {
|
||||||
Divider()
|
Divider()
|
||||||
@ -1060,7 +1013,6 @@ struct CategoryButton: View {
|
|||||||
RoundedRectangle(cornerRadius: 12)
|
RoundedRectangle(cornerRadius: 12)
|
||||||
.fill(isSelected ? Color.blue.opacity(0.15) : Color.clear)
|
.fill(isSelected ? Color.blue.opacity(0.15) : Color.clear)
|
||||||
)
|
)
|
||||||
.animation(.bouncy(duration: 0.3), value: isSelected)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1170,28 +1122,3 @@ struct InfoCard: View {
|
|||||||
.cornerRadius(8)
|
.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this code is used to enable the keyboard to be dismissed when scrolling if available on iOS 16+
|
|
||||||
extension View {
|
|
||||||
@ViewBuilder
|
|
||||||
func defaultScrollAnchorIsAvailable(_ anchor: UnitPoint?) -> some View {
|
|
||||||
if #available(iOS 17.0, *) {
|
|
||||||
self.defaultScrollAnchor(anchor)
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -32,22 +32,14 @@ struct MeloNXApp: App {
|
|||||||
@AppStorage("hasbeenfinished") var finishedStorage: Bool = false
|
@AppStorage("hasbeenfinished") var finishedStorage: Bool = false
|
||||||
|
|
||||||
@AppStorage("location-enabled") var locationenabled: 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
|
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
if finishedStorage {
|
if finishedStorage {
|
||||||
ContentView()
|
ContentView()
|
||||||
.withFileImporter()
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if checkForUpdate {
|
|
||||||
checkLatestVersion()
|
checkLatestVersion()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.sheet(isPresented: Binding(
|
.sheet(isPresented: Binding(
|
||||||
get: { showOutOfDateSheet && updateInfo != nil },
|
get: { showOutOfDateSheet && updateInfo != nil },
|
||||||
set: { newValue in
|
set: { newValue in
|
||||||
@ -64,13 +56,15 @@ struct MeloNXApp: App {
|
|||||||
} else {
|
} else {
|
||||||
SetupView(finished: $finished)
|
SetupView(finished: $finished)
|
||||||
.onChange(of: finished) { newValue in
|
.onChange(of: finished) { newValue in
|
||||||
withAnimation(.easeOut) {
|
withAnimation {
|
||||||
|
withAnimation {
|
||||||
finishedStorage = newValue
|
finishedStorage = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func checkLatestVersion() {
|
func checkLatestVersion() {
|
||||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
|
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
|
||||||
@ -79,7 +73,7 @@ struct MeloNXApp: App {
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
let urlString = "http://192.168.178.116:8000/api/latest_release"
|
let urlString = "http://192.168.178.116:8000/api/latest_release"
|
||||||
#else
|
#else
|
||||||
let urlString = "https://melonx.net/api/latest_release"
|
let urlString = "https://melonx.org/api/latest_release"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
guard let url = URL(string: urlString) else {
|
guard let url = URL(string: urlString) else {
|
||||||
|
@ -286,6 +286,16 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
{
|
{
|
||||||
_contentManager = new ContentManager(_virtualFileSystem);
|
_contentManager = new ContentManager(_virtualFileSystem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_accountManager == null)
|
||||||
|
{
|
||||||
|
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_userChannelPersistence == null)
|
||||||
|
{
|
||||||
|
_userChannelPersistence = new UserChannelPersistence();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Main(string[] args)
|
static void Main(string[] args)
|
||||||
@ -392,8 +402,8 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
return String.Empty;
|
return String.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "pause_emulation")]
|
[UnmanagedCallersOnly(EntryPoint = "stop_emulation")]
|
||||||
public static void PauseEmulation(bool shouldPause)
|
public static void StopEmulation(bool shouldPause)
|
||||||
{
|
{
|
||||||
if (_window != null)
|
if (_window != null)
|
||||||
{
|
{
|
||||||
@ -412,15 +422,6 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "stop_emulation")]
|
|
||||||
public static void StopEmulation()
|
|
||||||
{
|
|
||||||
if (_window != null)
|
|
||||||
{
|
|
||||||
_window.Exit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "get_game_info")]
|
[UnmanagedCallersOnly(EntryPoint = "get_game_info")]
|
||||||
public static GameInfoNative GetGameInfoNative(int descriptor, IntPtr extensionPtr)
|
public static GameInfoNative GetGameInfoNative(int descriptor, IntPtr extensionPtr)
|
||||||
{
|
{
|
||||||
@ -1132,23 +1133,43 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void Load(Options option)
|
static void Load(Options option)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (_virtualFileSystem == null)
|
||||||
|
{
|
||||||
|
_virtualFileSystem = VirtualFileSystem.CreateInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_libHacHorizonManager == null)
|
||||||
{
|
{
|
||||||
_libHacHorizonManager = new LibHacHorizonManager();
|
_libHacHorizonManager = new LibHacHorizonManager();
|
||||||
_libHacHorizonManager.InitializeFsServer(_virtualFileSystem);
|
_libHacHorizonManager.InitializeFsServer(_virtualFileSystem);
|
||||||
_libHacHorizonManager.InitializeArpServer();
|
_libHacHorizonManager.InitializeArpServer();
|
||||||
_libHacHorizonManager.InitializeBcatServer();
|
_libHacHorizonManager.InitializeBcatServer();
|
||||||
_libHacHorizonManager.InitializeSystemClients();
|
_libHacHorizonManager.InitializeSystemClients();
|
||||||
|
}
|
||||||
|
|
||||||
_contentManager = new ContentManager(_virtualFileSystem);
|
if (_contentManager == null)
|
||||||
|
|
||||||
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, option.UserProfile);
|
|
||||||
|
|
||||||
_userChannelPersistence = new UserChannelPersistence();
|
|
||||||
|
|
||||||
_inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver());
|
|
||||||
|
|
||||||
if (OperatingSystem.IsIOS())
|
|
||||||
{
|
{
|
||||||
|
_contentManager = new ContentManager(_virtualFileSystem);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_accountManager == null)
|
||||||
|
{
|
||||||
|
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, option.UserProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_userChannelPersistence == null)
|
||||||
|
{
|
||||||
|
_userChannelPersistence = new UserChannelPersistence();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_inputManager == null)
|
||||||
|
{
|
||||||
|
_inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OperatingSystem.IsIOS()) {
|
||||||
Logger.Info?.Print(LogClass.Application, $"Current Device: {option.DisplayName} ({option.DeviceModel}) {Environment.OSVersion.Version}");
|
Logger.Info?.Print(LogClass.Application, $"Current Device: {option.DisplayName} ({option.DeviceModel}) {Environment.OSVersion.Version}");
|
||||||
Logger.Info?.Print(LogClass.Application, $"Increased Memory Limit: {option.MemoryEnt}");
|
Logger.Info?.Print(LogClass.Application, $"Increased Memory Limit: {option.MemoryEnt}");
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user