Compare commits
7 Commits
Canary-1.2
...
Canary-1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1eb78872d8 | ||
|
|
fe9fe2a10f | ||
|
|
6ab899f621 | ||
|
|
faacec9801 | ||
|
|
55fdb3f6b2 | ||
|
|
1129ab0e8c | ||
|
|
b6b391b2cf |
@@ -7,6 +7,7 @@ namespace ARMeilleure.Memory
|
|||||||
public const int DefaultGranularity = 65536; // Mapping granularity in Windows.
|
public const int DefaultGranularity = 65536; // Mapping granularity in Windows.
|
||||||
|
|
||||||
public IJitMemoryBlock Block { get; }
|
public IJitMemoryBlock Block { get; }
|
||||||
|
public IJitMemoryAllocator Allocator { get; }
|
||||||
|
|
||||||
public nint Pointer => Block.Pointer;
|
public nint Pointer => Block.Pointer;
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ namespace ARMeilleure.Memory
|
|||||||
granularity = DefaultGranularity;
|
granularity = DefaultGranularity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Allocator = allocator;
|
||||||
Block = allocator.Reserve(maxSize);
|
Block = allocator.Reserve(maxSize);
|
||||||
_maxSize = maxSize;
|
_maxSize = maxSize;
|
||||||
_sizeGranularity = granularity;
|
_sizeGranularity = granularity;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ using ARMeilleure.CodeGen;
|
|||||||
using ARMeilleure.CodeGen.Unwinding;
|
using ARMeilleure.CodeGen.Unwinding;
|
||||||
using ARMeilleure.Memory;
|
using ARMeilleure.Memory;
|
||||||
using ARMeilleure.Native;
|
using ARMeilleure.Native;
|
||||||
|
using Humanizer;
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Memory;
|
using Ryujinx.Memory;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -18,9 +20,8 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
private static readonly int _pageMask = _pageSize - 1;
|
private static readonly int _pageMask = _pageSize - 1;
|
||||||
|
|
||||||
private const int CodeAlignment = 4; // Bytes.
|
private const int CodeAlignment = 4; // Bytes.
|
||||||
private const int CacheSize = 2047 * 1024 * 1024;
|
private const int CacheSize = 256 * 1024 * 1024;
|
||||||
|
|
||||||
private static ReservedRegion _jitRegion;
|
|
||||||
private static JitCacheInvalidation _jitCacheInvalidator;
|
private static JitCacheInvalidation _jitCacheInvalidator;
|
||||||
|
|
||||||
private static CacheMemoryAllocator _cacheAllocator;
|
private static CacheMemoryAllocator _cacheAllocator;
|
||||||
@@ -30,6 +31,9 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
private static readonly Lock _lock = new();
|
private static readonly Lock _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 nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize);
|
public static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize);
|
||||||
@@ -48,7 +52,9 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_jitRegion = new ReservedRegion(allocator, CacheSize);
|
ReservedRegion firstRegion = new(allocator, CacheSize);
|
||||||
|
_jitRegions.Add(firstRegion);
|
||||||
|
_activeRegionIndex = 0;
|
||||||
|
|
||||||
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
|
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
@@ -59,7 +65,9 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
|
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
JitUnwindWindows.InstallFunctionTableHandler(_jitRegion.Pointer, CacheSize, _jitRegion.Pointer + Allocate(_pageSize));
|
JitUnwindWindows.InstallFunctionTableHandler(
|
||||||
|
firstRegion.Pointer, CacheSize, firstRegion.Pointer + Allocate(_pageSize)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
@@ -75,8 +83,8 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
Debug.Assert(_initialized);
|
Debug.Assert(_initialized);
|
||||||
|
|
||||||
int funcOffset = Allocate(code.Length);
|
int funcOffset = Allocate(code.Length);
|
||||||
|
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
|
||||||
nint funcPtr = _jitRegion.Pointer + funcOffset;
|
nint funcPtr = targetRegion.Pointer + funcOffset;
|
||||||
|
|
||||||
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||||
{
|
{
|
||||||
@@ -90,9 +98,9 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ReprotectAsWritable(funcOffset, code.Length);
|
ReprotectAsWritable(targetRegion, funcOffset, code.Length);
|
||||||
Marshal.Copy(code, 0, funcPtr, code.Length);
|
Marshal.Copy(code, 0, funcPtr, code.Length);
|
||||||
ReprotectAsExecutable(funcOffset, code.Length);
|
ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
|
||||||
|
|
||||||
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||||
{
|
{
|
||||||
@@ -116,52 +124,83 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
{
|
{
|
||||||
Debug.Assert(_initialized);
|
Debug.Assert(_initialized);
|
||||||
|
|
||||||
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64());
|
foreach (ReservedRegion region in _jitRegions)
|
||||||
|
|
||||||
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
|
|
||||||
{
|
{
|
||||||
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
|
if (pointer.ToInt64() < region.Pointer.ToInt64() ||
|
||||||
_cacheEntries.RemoveAt(entryIndex);
|
pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
|
||||||
|
|
||||||
|
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
|
||||||
|
{
|
||||||
|
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
|
||||||
|
_cacheEntries.RemoveAt(entryIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ReprotectAsWritable(int offset, int size)
|
private static void ReprotectAsWritable(ReservedRegion region, 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;
|
||||||
|
|
||||||
_jitRegion.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ReprotectAsExecutable(int offset, int size)
|
private static void ReprotectAsExecutable(ReservedRegion region, 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;
|
||||||
|
|
||||||
_jitRegion.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int Allocate(int codeSize)
|
private static int Allocate(int codeSize)
|
||||||
{
|
{
|
||||||
codeSize = AlignCodeSize(codeSize);
|
codeSize = AlignCodeSize(codeSize);
|
||||||
|
|
||||||
int allocOffset = _cacheAllocator.Allocate(codeSize);
|
for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
|
||||||
|
|
||||||
if (allocOffset < 0)
|
|
||||||
{
|
{
|
||||||
throw new OutOfMemoryException("JIT Cache exhausted.");
|
int allocOffset = _cacheAllocator.Allocate(codeSize);
|
||||||
|
|
||||||
|
if (allocOffset >= 0)
|
||||||
|
{
|
||||||
|
_jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
||||||
|
_activeRegionIndex = i;
|
||||||
|
return allocOffset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
int exhaustedRegion = _activeRegionIndex;
|
||||||
|
var newRegion = new ReservedRegion(_jitRegions[0].Allocator, CacheSize);
|
||||||
|
_jitRegions.Add(newRegion);
|
||||||
|
_activeRegionIndex = _jitRegions.Count - 1;
|
||||||
|
|
||||||
|
int newRegionNumber = _activeRegionIndex;
|
||||||
|
|
||||||
return allocOffset;
|
Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {newRegionNumber} ({((newRegionNumber + 1) * CacheSize).Bytes()} Total Allocation).");
|
||||||
|
|
||||||
|
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
|
||||||
|
|
||||||
|
int allocOffsetNew = _cacheAllocator.Allocate(codeSize);
|
||||||
|
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)
|
private static int AlignCodeSize(int codeSize)
|
||||||
{
|
{
|
||||||
return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1);
|
return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1);
|
||||||
@@ -185,18 +224,21 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
|
foreach (ReservedRegion _ in _jitRegions)
|
||||||
|
|
||||||
if (index < 0)
|
|
||||||
{
|
{
|
||||||
index = ~index - 1;
|
int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
|
||||||
}
|
|
||||||
|
|
||||||
if (index >= 0)
|
if (index < 0)
|
||||||
{
|
{
|
||||||
entry = _cacheEntries[index];
|
index = ~index - 1;
|
||||||
entryIndex = index;
|
}
|
||||||
return true;
|
|
||||||
|
if (index >= 0)
|
||||||
|
{
|
||||||
|
entry = _cacheEntries[index];
|
||||||
|
entryIndex = index;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,17 +144,15 @@ namespace ARMeilleure.Translation.PTC
|
|||||||
|
|
||||||
public List<ulong> GetBlacklistedFunctions()
|
public List<ulong> GetBlacklistedFunctions()
|
||||||
{
|
{
|
||||||
List<ulong> funcs = new List<ulong>();
|
List<ulong> funcs = [];
|
||||||
|
|
||||||
foreach (var profiledFunc in ProfiledFuncs)
|
foreach ((ulong ptr, FuncProfile funcProfile) in ProfiledFuncs)
|
||||||
{
|
{
|
||||||
if (profiledFunc.Value.Blacklist)
|
if (!funcProfile.Blacklist)
|
||||||
{
|
continue;
|
||||||
if (!funcs.Contains(profiledFunc.Key))
|
|
||||||
{
|
if (!funcs.Contains(ptr))
|
||||||
funcs.Add(profiledFunc.Key);
|
funcs.Add(ptr);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return funcs;
|
return funcs;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using ARMeilleure.Memory;
|
using ARMeilleure.Memory;
|
||||||
|
using Humanizer;
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Memory;
|
using Ryujinx.Memory;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -15,9 +17,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
private static readonly int _pageMask = _pageSize - 1;
|
private static readonly int _pageMask = _pageSize - 1;
|
||||||
|
|
||||||
private const int CodeAlignment = 4; // Bytes.
|
private const int CodeAlignment = 4; // Bytes.
|
||||||
private const int CacheSize = 2047 * 1024 * 1024;
|
private const int CacheSize = 256 * 1024 * 1024;
|
||||||
|
|
||||||
private static ReservedRegion _jitRegion;
|
|
||||||
private static JitCacheInvalidation _jitCacheInvalidator;
|
private static JitCacheInvalidation _jitCacheInvalidator;
|
||||||
|
|
||||||
private static CacheMemoryAllocator _cacheAllocator;
|
private static CacheMemoryAllocator _cacheAllocator;
|
||||||
@@ -26,6 +27,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
|
|
||||||
private static readonly Lock _lock = new();
|
private static readonly Lock _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)]
|
||||||
@@ -45,7 +48,9 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_jitRegion = new ReservedRegion(allocator, CacheSize);
|
ReservedRegion firstRegion = new(allocator, CacheSize);
|
||||||
|
_jitRegions.Add(firstRegion);
|
||||||
|
_activeRegionIndex = 0;
|
||||||
|
|
||||||
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
|
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
@@ -65,8 +70,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
Debug.Assert(_initialized);
|
Debug.Assert(_initialized);
|
||||||
|
|
||||||
int funcOffset = Allocate(code.Length);
|
int funcOffset = Allocate(code.Length);
|
||||||
|
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
|
||||||
nint funcPtr = _jitRegion.Pointer + funcOffset;
|
nint funcPtr = targetRegion.Pointer + funcOffset;
|
||||||
|
|
||||||
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||||
{
|
{
|
||||||
@@ -80,18 +85,11 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ReprotectAsWritable(funcOffset, code.Length);
|
ReprotectAsWritable(targetRegion, funcOffset, code.Length);
|
||||||
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length));
|
Marshal.Copy(code.ToArray(), 0, funcPtr, code.Length);
|
||||||
ReprotectAsExecutable(funcOffset, code.Length);
|
ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
|
||||||
|
|
||||||
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
_jitCacheInvalidator?.Invalidate(funcPtr, (ulong)code.Length);
|
||||||
{
|
|
||||||
FlushInstructionCache(Process.GetCurrentProcess().Handle, funcPtr, (nuint)code.Length);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_jitCacheInvalidator?.Invalidate(funcPtr, (ulong)code.Length);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Add(funcOffset, code.Length);
|
Add(funcOffset, code.Length);
|
||||||
@@ -106,50 +104,80 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
{
|
{
|
||||||
Debug.Assert(_initialized);
|
Debug.Assert(_initialized);
|
||||||
|
|
||||||
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64());
|
foreach (ReservedRegion region in _jitRegions)
|
||||||
|
|
||||||
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
|
|
||||||
{
|
{
|
||||||
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
|
if (pointer.ToInt64() < region.Pointer.ToInt64() ||
|
||||||
_cacheEntries.RemoveAt(entryIndex);
|
pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
|
||||||
|
|
||||||
|
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
|
||||||
|
{
|
||||||
|
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
|
||||||
|
_cacheEntries.RemoveAt(entryIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ReprotectAsWritable(int offset, int size)
|
private static void ReprotectAsWritable(ReservedRegion region, 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;
|
||||||
|
|
||||||
_jitRegion.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ReprotectAsExecutable(int offset, int size)
|
private static void ReprotectAsExecutable(ReservedRegion region, 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;
|
||||||
|
|
||||||
_jitRegion.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int Allocate(int codeSize)
|
private static int Allocate(int codeSize)
|
||||||
{
|
{
|
||||||
codeSize = AlignCodeSize(codeSize);
|
codeSize = AlignCodeSize(codeSize);
|
||||||
|
|
||||||
int allocOffset = _cacheAllocator.Allocate(codeSize);
|
for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
|
||||||
|
|
||||||
if (allocOffset < 0)
|
|
||||||
{
|
{
|
||||||
throw new OutOfMemoryException("JIT Cache exhausted.");
|
int allocOffset = _cacheAllocator.Allocate(codeSize);
|
||||||
|
|
||||||
|
if (allocOffset >= 0)
|
||||||
|
{
|
||||||
|
_jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
||||||
|
_activeRegionIndex = i;
|
||||||
|
return allocOffset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
int exhaustedRegion = _activeRegionIndex;
|
||||||
|
ReservedRegion newRegion = new(_jitRegions[0].Allocator, CacheSize);
|
||||||
|
_jitRegions.Add(newRegion);
|
||||||
|
_activeRegionIndex = _jitRegions.Count - 1;
|
||||||
|
|
||||||
|
int newRegionNumber = _activeRegionIndex;
|
||||||
|
|
||||||
return allocOffset;
|
Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {newRegionNumber} ({((newRegionNumber + 1) * CacheSize).Bytes()} Total Allocation).");
|
||||||
|
|
||||||
|
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
|
||||||
|
|
||||||
|
int allocOffsetNew = _cacheAllocator.Allocate(codeSize);
|
||||||
|
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)
|
private static int AlignCodeSize(int codeSize)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
{
|
{
|
||||||
private const int CodeAlignment = 4; // Bytes.
|
private const int CodeAlignment = 4; // Bytes.
|
||||||
private const int SharedCacheSize = 2047 * 1024 * 1024;
|
private const int SharedCacheSize = 2047 * 1024 * 1024;
|
||||||
private const int LocalCacheSize = 128 * 1024 * 1024;
|
private const int LocalCacheSize = 256 * 1024 * 1024;
|
||||||
|
|
||||||
// How many calls to the same function we allow until we pad the shared cache to force the function to become available there
|
// How many calls to the same function we allow until we pad the shared cache to force the function to become available there
|
||||||
// and allow the guest to take the fast path.
|
// and allow the guest to take the fast path.
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Silk.NET.Vulkan;
|
using Silk.NET.Vulkan;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.Vulkan
|
namespace Ryujinx.Graphics.Vulkan
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Applets.Error
|
namespace Ryujinx.HLE.HOS.Applets.Error
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
|||||||
{ BsdSocketOption.SoLinger, SocketOptionName.Linger },
|
{ BsdSocketOption.SoLinger, SocketOptionName.Linger },
|
||||||
{ BsdSocketOption.SoOobInline, SocketOptionName.OutOfBandInline },
|
{ BsdSocketOption.SoOobInline, SocketOptionName.OutOfBandInline },
|
||||||
{ BsdSocketOption.SoReusePort, SocketOptionName.ReuseAddress },
|
{ BsdSocketOption.SoReusePort, SocketOptionName.ReuseAddress },
|
||||||
|
{ BsdSocketOption.SoNoSigpipe, SocketOptionName.DontLinger },
|
||||||
{ BsdSocketOption.SoSndBuf, SocketOptionName.SendBuffer },
|
{ BsdSocketOption.SoSndBuf, SocketOptionName.SendBuffer },
|
||||||
{ BsdSocketOption.SoRcvBuf, SocketOptionName.ReceiveBuffer },
|
{ BsdSocketOption.SoRcvBuf, SocketOptionName.ReceiveBuffer },
|
||||||
{ BsdSocketOption.SoSndLoWat, SocketOptionName.SendLowWater },
|
{ BsdSocketOption.SoSndLoWat, SocketOptionName.SendLowWater },
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using MsgPack;
|
|
||||||
using Ryujinx.Horizon.Common;
|
using Ryujinx.Horizon.Common;
|
||||||
using Ryujinx.Horizon.Prepo.Types;
|
using Ryujinx.Horizon.Prepo.Types;
|
||||||
using Ryujinx.Memory;
|
using Ryujinx.Memory;
|
||||||
|
|||||||
@@ -3350,26 +3350,101 @@
|
|||||||
{
|
{
|
||||||
"ID": "SettingsTabGeneralCheckUpdatesOnLaunch",
|
"ID": "SettingsTabGeneralCheckUpdatesOnLaunch",
|
||||||
"Translations": {
|
"Translations": {
|
||||||
"ar_SA": "التحقق من وجود تحديثات عند التشغيل",
|
"ar_SA": "",
|
||||||
"de_DE": "Beim Start nach Updates suchen",
|
"de_DE": "",
|
||||||
"el_GR": "Έλεγχος για Ενημερώσεις στην Εκκίνηση",
|
"el_GR": "",
|
||||||
"en_US": "Check for Updates on Launch",
|
"en_US": "Check for Updates:",
|
||||||
"es_ES": "Buscar actualizaciones al iniciar",
|
"es_ES": "",
|
||||||
"fr_FR": "Vérifier les mises à jour au démarrage",
|
"fr_FR": "",
|
||||||
"he_IL": "בדוק אם קיימים עדכונים בהפעלה",
|
"he_IL": "",
|
||||||
"it_IT": "Controlla aggiornamenti all'avvio",
|
"it_IT": "",
|
||||||
"ja_JP": "起動時にアップデートを確認する",
|
"ja_JP": "",
|
||||||
"ko_KR": "시작 시, 업데이트 확인",
|
"ko_KR": "",
|
||||||
"no_NO": "Se etter oppdateringer ved oppstart",
|
"no_NO": "",
|
||||||
"pl_PL": "Sprawdzaj aktualizacje przy uruchomieniu",
|
"pl_PL": "",
|
||||||
"pt_BR": "Verificar se há atualizações ao iniciar",
|
"pt_BR": "",
|
||||||
"ru_RU": "Проверять наличие обновлений при запуске",
|
"ru_RU": "",
|
||||||
"sv_SE": "Leta efter uppdatering vid uppstart",
|
"sv_SE": "",
|
||||||
"th_TH": "ตรวจหาการอัปเดตเมื่อเปิดโปรแกรม",
|
"th_TH": "",
|
||||||
"tr_TR": "Her Açılışta Güncellemeleri Denetle",
|
"tr_TR": "",
|
||||||
"uk_UA": "Перевіряти наявність оновлень під час запуску",
|
"uk_UA": "",
|
||||||
"zh_CN": "启动时检查更新",
|
"zh_CN": "",
|
||||||
"zh_TW": "啟動時檢查更新"
|
"zh_TW": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": "SettingsTabGeneralCheckUpdatesOnLaunchOff",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Off",
|
||||||
|
"es_ES": "",
|
||||||
|
"fr_FR": "",
|
||||||
|
"he_IL": "",
|
||||||
|
"it_IT": "",
|
||||||
|
"ja_JP": "",
|
||||||
|
"ko_KR": "",
|
||||||
|
"no_NO": "",
|
||||||
|
"pl_PL": "",
|
||||||
|
"pt_BR": "",
|
||||||
|
"ru_RU": "",
|
||||||
|
"sv_SE": "",
|
||||||
|
"th_TH": "",
|
||||||
|
"tr_TR": "",
|
||||||
|
"uk_UA": "",
|
||||||
|
"zh_CN": "",
|
||||||
|
"zh_TW": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": "SettingsTabGeneralCheckUpdatesOnLaunchPromptAtStartup",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Prompt",
|
||||||
|
"es_ES": "",
|
||||||
|
"fr_FR": "",
|
||||||
|
"he_IL": "",
|
||||||
|
"it_IT": "",
|
||||||
|
"ja_JP": "",
|
||||||
|
"ko_KR": "",
|
||||||
|
"no_NO": "",
|
||||||
|
"pl_PL": "",
|
||||||
|
"pt_BR": "",
|
||||||
|
"ru_RU": "",
|
||||||
|
"sv_SE": "",
|
||||||
|
"th_TH": "",
|
||||||
|
"tr_TR": "",
|
||||||
|
"uk_UA": "",
|
||||||
|
"zh_CN": "",
|
||||||
|
"zh_TW": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": "SettingsTabGeneralCheckUpdatesOnLaunchBackground",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Background",
|
||||||
|
"es_ES": "",
|
||||||
|
"fr_FR": "",
|
||||||
|
"he_IL": "",
|
||||||
|
"it_IT": "",
|
||||||
|
"ja_JP": "",
|
||||||
|
"ko_KR": "",
|
||||||
|
"no_NO": "",
|
||||||
|
"pl_PL": "",
|
||||||
|
"pt_BR": "",
|
||||||
|
"ru_RU": "",
|
||||||
|
"sv_SE": "",
|
||||||
|
"th_TH": "",
|
||||||
|
"tr_TR": "",
|
||||||
|
"uk_UA": "",
|
||||||
|
"zh_CN": "",
|
||||||
|
"zh_TW": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using DiscordRPC;
|
using DiscordRPC;
|
||||||
using Gommon;
|
using Gommon;
|
||||||
using MsgPack;
|
|
||||||
using Ryujinx.Ava.Utilities;
|
using Ryujinx.Ava.Utilities;
|
||||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||||
using Ryujinx.Ava.Utilities.Configuration;
|
using Ryujinx.Ava.Utilities.Configuration;
|
||||||
@@ -11,7 +10,6 @@ using Ryujinx.HLE;
|
|||||||
using Ryujinx.HLE.Loaders.Processes;
|
using Ryujinx.HLE.Loaders.Processes;
|
||||||
using Ryujinx.Horizon;
|
using Ryujinx.Horizon;
|
||||||
using Ryujinx.Horizon.Prepo.Types;
|
using Ryujinx.Horizon.Prepo.Types;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Ryujinx.Ava
|
namespace Ryujinx.Ava
|
||||||
|
|||||||
@@ -387,7 +387,7 @@ namespace Ryujinx.Headless
|
|||||||
[Option("graphics-shaders-dump-path", Required = false, HelpText = "Dumps shaders in this local directory. (Developer only)")]
|
[Option("graphics-shaders-dump-path", Required = false, HelpText = "Dumps shaders in this local directory. (Developer only)")]
|
||||||
public string GraphicsShadersDumpPath { get; set; }
|
public string GraphicsShadersDumpPath { get; set; }
|
||||||
|
|
||||||
[Option("graphics-backend", Required = false, Default = GraphicsBackend.OpenGl, HelpText = "Change Graphics Backend to use.")]
|
[Option("graphics-backend", Required = false, Default = GraphicsBackend.Vulkan, HelpText = "Change Graphics Backend to use.")]
|
||||||
public GraphicsBackend GraphicsBackend { get; set; }
|
public GraphicsBackend GraphicsBackend { get; set; }
|
||||||
|
|
||||||
[Option("preferred-gpu-vendor", Required = false, Default = "", HelpText = "When using the Vulkan backend, prefer using the GPU from the specified vendor.")]
|
[Option("preferred-gpu-vendor", Required = false, Default = "", HelpText = "When using the Vulkan backend, prefer using the GPU from the specified vendor.")]
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
using Avalonia;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Input.Platform;
|
using Avalonia.Input.Platform;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Ryujinx.Ava;
|
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.UI.Controls;
|
|
||||||
using Ryujinx.Ava.UI.Helpers;
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using Ryujinx.Ava.UI.Windows;
|
using Ryujinx.Ava.UI.Windows;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using Ryujinx.Ava.UI.ViewModels;
|
|||||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||||
using Ryujinx.Ava.Utilities.Compat;
|
using Ryujinx.Ava.Utilities.Compat;
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Controls
|
namespace Ryujinx.Ava.UI.Controls
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using Ryujinx.Ava.UI.Models.Input;
|
|||||||
using Ryujinx.Ava.UI.Windows;
|
using Ryujinx.Ava.UI.Windows;
|
||||||
using Ryujinx.Ava.Utilities.Configuration;
|
using Ryujinx.Ava.Utilities.Configuration;
|
||||||
using Ryujinx.Ava.Utilities.Configuration.System;
|
using Ryujinx.Ava.Utilities.Configuration.System;
|
||||||
|
using Ryujinx.Ava.Utilities.Configuration.UI;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Configuration.Multiplayer;
|
using Ryujinx.Common.Configuration.Multiplayer;
|
||||||
using Ryujinx.Common.GraphicsDriver;
|
using Ryujinx.Common.GraphicsDriver;
|
||||||
@@ -121,6 +122,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
public bool RememberWindowState { get; set; }
|
public bool RememberWindowState { get; set; }
|
||||||
public bool ShowTitleBar { get; set; }
|
public bool ShowTitleBar { get; set; }
|
||||||
public int HideCursor { get; set; }
|
public int HideCursor { get; set; }
|
||||||
|
public int UpdateCheckerType { get; set; }
|
||||||
public bool EnableDockedMode { get; set; }
|
public bool EnableDockedMode { get; set; }
|
||||||
public bool EnableKeyboard { get; set; }
|
public bool EnableKeyboard { get; set; }
|
||||||
public bool EnableMouse { get; set; }
|
public bool EnableMouse { get; set; }
|
||||||
@@ -476,6 +478,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
RememberWindowState = config.RememberWindowState;
|
RememberWindowState = config.RememberWindowState;
|
||||||
ShowTitleBar = config.ShowTitleBar;
|
ShowTitleBar = config.ShowTitleBar;
|
||||||
HideCursor = (int)config.HideCursor.Value;
|
HideCursor = (int)config.HideCursor.Value;
|
||||||
|
UpdateCheckerType = (int)config.UpdateCheckerType.Value;
|
||||||
|
|
||||||
GameDirectories.Clear();
|
GameDirectories.Clear();
|
||||||
GameDirectories.AddRange(config.UI.GameDirs.Value);
|
GameDirectories.AddRange(config.UI.GameDirs.Value);
|
||||||
@@ -582,6 +585,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
config.RememberWindowState.Value = RememberWindowState;
|
config.RememberWindowState.Value = RememberWindowState;
|
||||||
config.ShowTitleBar.Value = ShowTitleBar;
|
config.ShowTitleBar.Value = ShowTitleBar;
|
||||||
config.HideCursor.Value = (HideCursorMode)HideCursor;
|
config.HideCursor.Value = (HideCursorMode)HideCursor;
|
||||||
|
config.UpdateCheckerType.Value = (UpdaterType)UpdateCheckerType;
|
||||||
|
|
||||||
if (GameDirectoryChanged)
|
if (GameDirectoryChanged)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;
|
using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||||
|
xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
x:DataType="viewModels:SettingsViewModel">
|
x:DataType="viewModels:SettingsViewModel">
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
@@ -30,18 +31,33 @@
|
|||||||
ToolTip.Tip="{ext:Locale ToggleDiscordTooltip}"
|
ToolTip.Tip="{ext:Locale ToggleDiscordTooltip}"
|
||||||
Text="{ext:Locale SettingsTabGeneralEnableDiscordRichPresence}" />
|
Text="{ext:Locale SettingsTabGeneralEnableDiscordRichPresence}" />
|
||||||
</CheckBox>
|
</CheckBox>
|
||||||
<CheckBox IsChecked="{Binding CheckUpdatesOnStart}">
|
|
||||||
<TextBlock Text="{ext:Locale SettingsTabGeneralCheckUpdatesOnLaunch}" />
|
|
||||||
</CheckBox>
|
|
||||||
<CheckBox IsChecked="{Binding ShowConfirmExit}">
|
<CheckBox IsChecked="{Binding ShowConfirmExit}">
|
||||||
<TextBlock Text="{ext:Locale SettingsTabGeneralShowConfirmExitDialog}" />
|
<TextBlock Text="{ext:Locale SettingsTabGeneralShowConfirmExitDialog}" />
|
||||||
</CheckBox>
|
</CheckBox>
|
||||||
<CheckBox IsChecked="{Binding RememberWindowState}">
|
<CheckBox IsChecked="{Binding RememberWindowState}">
|
||||||
<TextBlock Text="{ext:Locale SettingsTabGeneralRememberWindowState}" />
|
<TextBlock Text="{ext:Locale SettingsTabGeneralRememberWindowState}" />
|
||||||
</CheckBox>
|
</CheckBox>
|
||||||
<CheckBox IsChecked="{Binding ShowTitleBar}" Name="ShowTitleBarBox">
|
<CheckBox IsChecked="{Binding ShowTitleBar}" IsVisible="{x:Static helper:RunningPlatform.IsWindows}">
|
||||||
<TextBlock Text="{ext:Locale SettingsTabGeneralShowTitleBar}" />
|
<TextBlock Text="{ext:Locale SettingsTabGeneralShowTitleBar}" />
|
||||||
</CheckBox>
|
</CheckBox>
|
||||||
|
<StackPanel Margin="0, 15, 0, 0" Orientation="Horizontal">
|
||||||
|
<TextBlock VerticalAlignment="Center"
|
||||||
|
Text="{ext:Locale SettingsTabGeneralCheckUpdatesOnLaunch}"
|
||||||
|
Width="150" />
|
||||||
|
<ComboBox SelectedIndex="{Binding UpdateCheckerType}"
|
||||||
|
HorizontalContentAlignment="Left"
|
||||||
|
MinWidth="100">
|
||||||
|
<ComboBoxItem>
|
||||||
|
<TextBlock Text="{ext:Locale SettingsTabGeneralCheckUpdatesOnLaunchOff}" />
|
||||||
|
</ComboBoxItem>
|
||||||
|
<ComboBoxItem>
|
||||||
|
<TextBlock Text="{ext:Locale SettingsTabGeneralCheckUpdatesOnLaunchPromptAtStartup}" />
|
||||||
|
</ComboBoxItem>
|
||||||
|
<ComboBoxItem>
|
||||||
|
<TextBlock Text="{ext:Locale SettingsTabGeneralCheckUpdatesOnLaunchBackground}" />
|
||||||
|
</ComboBoxItem>
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
<StackPanel Margin="0, 15, 0, 0" Orientation="Horizontal">
|
<StackPanel Margin="0, 15, 0, 0" Orientation="Horizontal">
|
||||||
<TextBlock VerticalAlignment="Center"
|
<TextBlock VerticalAlignment="Center"
|
||||||
Text="{ext:Locale SettingsTabGeneralHideCursor}"
|
Text="{ext:Locale SettingsTabGeneralHideCursor}"
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ namespace Ryujinx.Ava.UI.Views.Settings
|
|||||||
public SettingsUiView()
|
public SettingsUiView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
ShowTitleBarBox.IsVisible = OperatingSystem.IsWindows();
|
|
||||||
AddGameDirButton.Command =
|
AddGameDirButton.Command =
|
||||||
Commands.Create(() => AddDirButton(GameDirPathBox, ViewModel.GameDirectories, true));
|
Commands.Create(() => AddDirButton(GameDirPathBox, ViewModel.GameDirectories, true));
|
||||||
AddAutoloadDirButton.Command =
|
AddAutoloadDirButton.Command =
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ using Ryujinx.Ava.UI.ViewModels;
|
|||||||
using Ryujinx.Ava.Utilities;
|
using Ryujinx.Ava.Utilities;
|
||||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||||
using Ryujinx.Ava.Utilities.Configuration;
|
using Ryujinx.Ava.Utilities.Configuration;
|
||||||
|
using Ryujinx.Ava.Utilities.Configuration.UI;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Helper;
|
using Ryujinx.Common.Helper;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
@@ -400,10 +401,29 @@ namespace Ryujinx.Ava.UI.Windows
|
|||||||
await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys));
|
await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ConfigurationState.Instance.CheckUpdatesOnStart && !CommandLineState.HideAvailableUpdates && Updater.CanUpdate())
|
if (!Updater.CanUpdate() || CommandLineState.HideAvailableUpdates)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (ConfigurationState.Instance.UpdateCheckerType.Value)
|
||||||
{
|
{
|
||||||
await Updater.BeginUpdateAsync()
|
case UpdaterType.PromptAtStartup:
|
||||||
.Catch(task => Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}"));
|
await Updater.BeginUpdateAsync()
|
||||||
|
.Catch(task => Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}"));
|
||||||
|
break;
|
||||||
|
case UpdaterType.CheckInBackground:
|
||||||
|
if ((await Updater.CheckVersionAsync()).TryGet(out (Version Current, Version Incoming) versions))
|
||||||
|
{
|
||||||
|
string newVersionString = ReleaseInformation.IsCanaryBuild
|
||||||
|
? $"Canary {versions.Current} -> Canary {versions.Incoming}"
|
||||||
|
: $"{versions.Current} -> {versions.Incoming}";
|
||||||
|
|
||||||
|
if (versions.Current < versions.Incoming)
|
||||||
|
NotificationHelper.ShowInformation(
|
||||||
|
title: "Update Available",
|
||||||
|
text: newVersionString,
|
||||||
|
onClick: () => _ = Updater.BeginUpdateAsync());
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,18 @@ namespace Ryujinx.Ava
|
|||||||
private const int ConnectionCount = 4;
|
private const int ConnectionCount = 4;
|
||||||
|
|
||||||
private static string _buildVer;
|
private static string _buildVer;
|
||||||
private static string _platformExt;
|
|
||||||
|
private static readonly string _platformExt =
|
||||||
|
RunningPlatform.IsMacOS
|
||||||
|
? "macos_universal.app.tar.gz"
|
||||||
|
: RunningPlatform.IsWindows
|
||||||
|
? "win_x64.zip"
|
||||||
|
: RunningPlatform.IsX64Linux
|
||||||
|
? "linux_x64.tar.gz"
|
||||||
|
: RunningPlatform.IsArmLinux
|
||||||
|
? "linux_arm64.tar.gz"
|
||||||
|
: throw new PlatformNotSupportedException();
|
||||||
|
|
||||||
private static string _buildUrl;
|
private static string _buildUrl;
|
||||||
private static long _buildSize;
|
private static long _buildSize;
|
||||||
private static bool _updateSuccessful;
|
private static bool _updateSuccessful;
|
||||||
@@ -51,30 +62,8 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
private static readonly string[] _windowsDependencyDirs = [];
|
private static readonly string[] _windowsDependencyDirs = [];
|
||||||
|
|
||||||
public static async Task BeginUpdateAsync(bool showVersionUpToDate = false)
|
public static async Task<Optional<(Version Current, Version Incoming)>> CheckVersionAsync(bool showVersionUpToDate = false)
|
||||||
{
|
{
|
||||||
if (_running)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_running = true;
|
|
||||||
|
|
||||||
// Detect current platform
|
|
||||||
if (OperatingSystem.IsMacOS())
|
|
||||||
{
|
|
||||||
_platformExt = "macos_universal.app.tar.gz";
|
|
||||||
}
|
|
||||||
else if (OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
_platformExt = "win_x64.zip";
|
|
||||||
}
|
|
||||||
else if (OperatingSystem.IsLinux())
|
|
||||||
{
|
|
||||||
string arch = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "arm64" : "x64";
|
|
||||||
_platformExt = $"linux_{arch}.tar.gz";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Version.TryParse(Program.Version, out Version currentVersion))
|
if (!Version.TryParse(Program.Version, out Version currentVersion))
|
||||||
{
|
{
|
||||||
Logger.Error?.Print(LogClass.Application, $"Failed to convert the current {RyujinxApp.FullAppName} version!");
|
Logger.Error?.Print(LogClass.Application, $"Failed to convert the current {RyujinxApp.FullAppName} version!");
|
||||||
@@ -85,7 +74,7 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
_running = false;
|
_running = false;
|
||||||
|
|
||||||
return;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Info?.Print(LogClass.Application, "Checking for updates.");
|
Logger.Info?.Print(LogClass.Application, "Checking for updates.");
|
||||||
@@ -123,7 +112,7 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
_running = false;
|
_running = false;
|
||||||
|
|
||||||
return;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -149,7 +138,7 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
_running = false;
|
_running = false;
|
||||||
|
|
||||||
return;
|
return default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
@@ -161,7 +150,7 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
_running = false;
|
_running = false;
|
||||||
|
|
||||||
return;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Version.TryParse(_buildVer, out Version newVersion))
|
if (!Version.TryParse(_buildVer, out Version newVersion))
|
||||||
@@ -174,9 +163,23 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
_running = false;
|
_running = false;
|
||||||
|
|
||||||
|
return (currentVersion, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (currentVersion, newVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task BeginUpdateAsync(bool showVersionUpToDate = false)
|
||||||
|
{
|
||||||
|
if (_running)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_running = true;
|
||||||
|
|
||||||
|
(Version currentVersion, Version newVersion) = (await CheckVersionAsync(showVersionUpToDate)).OrDefault();
|
||||||
|
|
||||||
if (newVersion <= currentVersion)
|
if (newVersion <= currentVersion)
|
||||||
{
|
{
|
||||||
if (showVersionUpToDate)
|
if (showVersionUpToDate)
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
|
|||||||
|
|
||||||
public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
|
public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
|
||||||
|
|
||||||
public bool HasPlayedPreviously => TimePlayedString != string.Empty;
|
public bool HasPlayedPreviously => TimePlayed.TotalSeconds > 1;
|
||||||
|
|
||||||
public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed)?.Replace(" ", "\n");
|
public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed)?.Replace(" ", "\n");
|
||||||
|
|
||||||
@@ -78,25 +78,6 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
|
|||||||
|
|
||||||
public LocaleKeys? PlayabilityStatus => Compatibility.Convert(x => x.Status).OrElse(null);
|
public LocaleKeys? PlayabilityStatus => Compatibility.Convert(x => x.Status).OrElse(null);
|
||||||
|
|
||||||
public string CompatibilityToolTip
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
StringBuilder sb = new(LocalizedStatusTooltip);
|
|
||||||
|
|
||||||
string formattedCompatibilityLabels = FormattedCompatibilityLabels;
|
|
||||||
if (!string.IsNullOrEmpty(formattedCompatibilityLabels))
|
|
||||||
{
|
|
||||||
sb.AppendLine()
|
|
||||||
.AppendLine()
|
|
||||||
.Append(formattedCompatibilityLabels);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public string LocalizedStatusTooltip =>
|
public string LocalizedStatusTooltip =>
|
||||||
Compatibility.Convert(x =>
|
Compatibility.Convert(x =>
|
||||||
#pragma warning disable CS8509 It is exhaustive for all possible values this can contain.
|
#pragma warning disable CS8509 It is exhaustive for all possible values this can contain.
|
||||||
@@ -120,16 +101,16 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
|
|||||||
|
|
||||||
public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath)
|
public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath)
|
||||||
{
|
{
|
||||||
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
|
|
||||||
|
|
||||||
Nca mainNca = null;
|
|
||||||
Nca patchNca = null;
|
|
||||||
|
|
||||||
if (!System.IO.Path.Exists(titleFilePath))
|
if (!System.IO.Path.Exists(titleFilePath))
|
||||||
{
|
{
|
||||||
Logger.Error?.Print(LogClass.Application, $"File \"{titleFilePath}\" does not exist.");
|
Logger.Error?.Print(LogClass.Application, $"File \"{titleFilePath}\" does not exist.");
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
|
||||||
|
|
||||||
|
Nca mainNca = null;
|
||||||
|
Nca patchNca = null;
|
||||||
|
|
||||||
string extension = System.IO.Path.GetExtension(titleFilePath).ToLower();
|
string extension = System.IO.Path.GetExtension(titleFilePath).ToLower();
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The current version of the file format
|
/// The current version of the file format
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const int CurrentVersion = 64;
|
public const int CurrentVersion = 65;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Version of the configuration file format
|
/// Version of the configuration file format
|
||||||
@@ -163,9 +163,14 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
|||||||
public bool EnableDiscordIntegration { get; set; }
|
public bool EnableDiscordIntegration { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks for updates when Ryujinx starts when enabled
|
/// DEPRECATED: Checks for updates when Ryujinx starts when enabled
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool CheckUpdatesOnStart { get; set; }
|
public bool CheckUpdatesOnStart { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks for updates when Ryujinx starts when enabled, either prompting when an update is found or just showing a notification.
|
||||||
|
/// </summary>
|
||||||
|
public UpdaterType UpdateCheckerType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Show "Confirm Exit" Dialog
|
/// Show "Confirm Exit" Dialog
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
|||||||
|
|
||||||
EnableDiscordIntegration.Value = cff.EnableDiscordIntegration;
|
EnableDiscordIntegration.Value = cff.EnableDiscordIntegration;
|
||||||
CheckUpdatesOnStart.Value = cff.CheckUpdatesOnStart;
|
CheckUpdatesOnStart.Value = cff.CheckUpdatesOnStart;
|
||||||
|
UpdateCheckerType.Value = cff.UpdateCheckerType;
|
||||||
ShowConfirmExit.Value = cff.ShowConfirmExit;
|
ShowConfirmExit.Value = cff.ShowConfirmExit;
|
||||||
RememberWindowState.Value = cff.RememberWindowState;
|
RememberWindowState.Value = cff.RememberWindowState;
|
||||||
ShowTitleBar.Value = cff.ShowTitleBar;
|
ShowTitleBar.Value = cff.ShowTitleBar;
|
||||||
@@ -431,7 +432,8 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
|||||||
}),
|
}),
|
||||||
(62, static cff => cff.RainbowSpeed = 1f),
|
(62, static cff => cff.RainbowSpeed = 1f),
|
||||||
(63, static cff => cff.MatchSystemTime = false),
|
(63, static cff => cff.MatchSystemTime = false),
|
||||||
(64, static cff => cff.LoggingEnableAvalonia = false)
|
(64, static cff => cff.LoggingEnableAvalonia = false),
|
||||||
|
(65, static cff => cff.UpdateCheckerType = cff.CheckUpdatesOnStart ? UpdaterType.PromptAtStartup : UpdaterType.Off)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using ARMeilleure;
|
using ARMeilleure;
|
||||||
using Gommon;
|
using Gommon;
|
||||||
using Ryujinx.Ava.Utilities.Configuration.System;
|
using Ryujinx.Ava.Utilities.Configuration.System;
|
||||||
|
using Ryujinx.Ava.Utilities.Configuration.UI;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Configuration.Hid;
|
using Ryujinx.Common.Configuration.Hid;
|
||||||
@@ -767,6 +768,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
|||||||
/// Checks for updates when Ryujinx starts when enabled
|
/// Checks for updates when Ryujinx starts when enabled
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ReactiveObject<bool> CheckUpdatesOnStart { get; private set; }
|
public ReactiveObject<bool> CheckUpdatesOnStart { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks for updates when Ryujinx starts when enabled, either prompting when an update is found or just showing a notification.
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveObject<UpdaterType> UpdateCheckerType { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Show "Confirm Exit" Dialog
|
/// Show "Confirm Exit" Dialog
|
||||||
@@ -804,6 +810,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
|||||||
Hacks = new HacksSection();
|
Hacks = new HacksSection();
|
||||||
EnableDiscordIntegration = new ReactiveObject<bool>();
|
EnableDiscordIntegration = new ReactiveObject<bool>();
|
||||||
CheckUpdatesOnStart = new ReactiveObject<bool>();
|
CheckUpdatesOnStart = new ReactiveObject<bool>();
|
||||||
|
UpdateCheckerType = new ReactiveObject<UpdaterType>();
|
||||||
ShowConfirmExit = new ReactiveObject<bool>();
|
ShowConfirmExit = new ReactiveObject<bool>();
|
||||||
RememberWindowState = new ReactiveObject<bool>();
|
RememberWindowState = new ReactiveObject<bool>();
|
||||||
ShowTitleBar = new ReactiveObject<bool>();
|
ShowTitleBar = new ReactiveObject<bool>();
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
|||||||
DockedMode = System.EnableDockedMode,
|
DockedMode = System.EnableDockedMode,
|
||||||
EnableDiscordIntegration = EnableDiscordIntegration,
|
EnableDiscordIntegration = EnableDiscordIntegration,
|
||||||
CheckUpdatesOnStart = CheckUpdatesOnStart,
|
CheckUpdatesOnStart = CheckUpdatesOnStart,
|
||||||
|
UpdateCheckerType = UpdateCheckerType,
|
||||||
ShowConfirmExit = ShowConfirmExit,
|
ShowConfirmExit = ShowConfirmExit,
|
||||||
RememberWindowState = RememberWindowState,
|
RememberWindowState = RememberWindowState,
|
||||||
ShowTitleBar = ShowTitleBar,
|
ShowTitleBar = ShowTitleBar,
|
||||||
@@ -175,7 +176,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
|||||||
System.SystemTimeOffset.Value = 0;
|
System.SystemTimeOffset.Value = 0;
|
||||||
System.EnableDockedMode.Value = true;
|
System.EnableDockedMode.Value = true;
|
||||||
EnableDiscordIntegration.Value = true;
|
EnableDiscordIntegration.Value = true;
|
||||||
CheckUpdatesOnStart.Value = true;
|
UpdateCheckerType.Value = UpdaterType.PromptAtStartup;
|
||||||
ShowConfirmExit.Value = true;
|
ShowConfirmExit.Value = true;
|
||||||
RememberWindowState.Value = true;
|
RememberWindowState.Value = true;
|
||||||
ShowTitleBar.Value = !OperatingSystem.IsWindows();
|
ShowTitleBar.Value = !OperatingSystem.IsWindows();
|
||||||
|
|||||||
13
src/Ryujinx/Utilities/Configuration/UI/UpdaterType.cs
Normal file
13
src/Ryujinx/Utilities/Configuration/UI/UpdaterType.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using Ryujinx.Common.Utilities;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Utilities.Configuration.UI
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(TypedStringEnumConverter<UpdaterType>))]
|
||||||
|
public enum UpdaterType
|
||||||
|
{
|
||||||
|
Off,
|
||||||
|
PromptAtStartup,
|
||||||
|
CheckInBackground
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using Gommon;
|
using Gommon;
|
||||||
using MsgPack;
|
|
||||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using MsgPack;
|
using MsgPack;
|
||||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Utilities.PlayReport
|
namespace Ryujinx.Ava.Utilities.PlayReport
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
using System;
|
namespace Ryujinx.Ava.Utilities.PlayReport
|
||||||
using System.Buffers.Binary;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Utilities.PlayReport
|
|
||||||
{
|
{
|
||||||
public static partial class PlayReports
|
public static partial class PlayReports
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using FluentAvalonia.Core;
|
using MsgPack;
|
||||||
using MsgPack;
|
|
||||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -153,7 +152,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
|
|||||||
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
|
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
|
||||||
{
|
{
|
||||||
List<MessagePackObject> packedObjects = [];
|
List<MessagePackObject> packedObjects = [];
|
||||||
foreach (var reportKey in ReportKeys)
|
foreach (string reportKey in ReportKeys)
|
||||||
{
|
{
|
||||||
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
|
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
|
||||||
{
|
{
|
||||||
@@ -177,7 +176,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
|
|||||||
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
|
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
|
||||||
{
|
{
|
||||||
Dictionary<string, MessagePackObject> packedObjects = [];
|
Dictionary<string, MessagePackObject> packedObjects = [];
|
||||||
foreach (var reportKey in ReportKeys)
|
foreach (string reportKey in ReportKeys)
|
||||||
{
|
{
|
||||||
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
|
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
Reference in New Issue
Block a user