Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c8edaf89e |
@@ -7,7 +7,6 @@ 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;
|
||||||
|
|
||||||
@@ -22,7 +21,6 @@ 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,8 +2,6 @@ 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;
|
||||||
@@ -20,8 +18,9 @@ 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 = 128 * 1024 * 1024;
|
private const int CacheSize = 2047 * 1024 * 1024;
|
||||||
|
|
||||||
|
private static ReservedRegion _jitRegion;
|
||||||
private static JitCacheInvalidation _jitCacheInvalidator;
|
private static JitCacheInvalidation _jitCacheInvalidator;
|
||||||
|
|
||||||
private static CacheMemoryAllocator _cacheAllocator;
|
private static CacheMemoryAllocator _cacheAllocator;
|
||||||
@@ -31,9 +30,6 @@ 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);
|
||||||
@@ -52,9 +48,7 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var firstRegion = new ReservedRegion(allocator, CacheSize);
|
_jitRegion = new ReservedRegion(allocator, CacheSize);
|
||||||
_jitRegions.Add(firstRegion);
|
|
||||||
_activeRegionIndex = 0;
|
|
||||||
|
|
||||||
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
|
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
@@ -65,9 +59,7 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
|
|
||||||
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;
|
||||||
@@ -83,8 +75,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 = targetRegion.Pointer + funcOffset;
|
nint funcPtr = _jitRegion.Pointer + funcOffset;
|
||||||
|
|
||||||
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||||
{
|
{
|
||||||
@@ -98,9 +90,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)
|
||||||
{
|
{
|
||||||
@@ -124,83 +116,52 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
{
|
{
|
||||||
Debug.Assert(_initialized);
|
Debug.Assert(_initialized);
|
||||||
|
|
||||||
foreach (var region in _jitRegions)
|
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64());
|
||||||
|
|
||||||
|
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
|
||||||
{
|
{
|
||||||
if (pointer.ToInt64() < region.Pointer.ToInt64() ||
|
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
|
||||||
pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
|
_cacheEntries.RemoveAt(entryIndex);
|
||||||
{
|
|
||||||
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(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)
|
private static int Allocate(int codeSize)
|
||||||
{
|
{
|
||||||
codeSize = AlignCodeSize(codeSize);
|
codeSize = AlignCodeSize(codeSize);
|
||||||
|
|
||||||
for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
|
int allocOffset = _cacheAllocator.Allocate(codeSize);
|
||||||
|
|
||||||
|
if (allocOffset < 0)
|
||||||
{
|
{
|
||||||
int allocOffset = _cacheAllocator.Allocate(codeSize);
|
throw new OutOfMemoryException("JIT Cache exhausted.");
|
||||||
|
|
||||||
if (allocOffset >= 0)
|
|
||||||
{
|
|
||||||
_jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
|
||||||
_activeRegionIndex = i;
|
|
||||||
return allocOffset;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int exhaustedRegion = _activeRegionIndex;
|
_jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
||||||
var newRegion = new ReservedRegion(_jitRegions[0].Allocator, CacheSize);
|
|
||||||
_jitRegions.Add(newRegion);
|
|
||||||
_activeRegionIndex = _jitRegions.Count - 1;
|
|
||||||
|
|
||||||
int newRegionNumber = _activeRegionIndex;
|
|
||||||
|
|
||||||
Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {newRegionNumber} ({((newRegionNumber + 1) * CacheSize).Bytes()} Total Allocation).");
|
return allocOffset;
|
||||||
|
|
||||||
_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);
|
||||||
@@ -224,21 +185,18 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
foreach (var region in _jitRegions)
|
int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
|
||||||
|
|
||||||
|
if (index < 0)
|
||||||
{
|
{
|
||||||
int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
|
index = ~index - 1;
|
||||||
|
}
|
||||||
|
|
||||||
if (index < 0)
|
if (index >= 0)
|
||||||
{
|
{
|
||||||
index = ~index - 1;
|
entry = _cacheEntries[index];
|
||||||
}
|
entryIndex = index;
|
||||||
|
return true;
|
||||||
if (index >= 0)
|
|
||||||
{
|
|
||||||
entry = _cacheEntries[index];
|
|
||||||
entryIndex = index;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
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;
|
||||||
@@ -17,8 +15,9 @@ 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 = 128 * 1024 * 1024;
|
private const int CacheSize = 2047 * 1024 * 1024;
|
||||||
|
|
||||||
|
private static ReservedRegion _jitRegion;
|
||||||
private static JitCacheInvalidation _jitCacheInvalidator;
|
private static JitCacheInvalidation _jitCacheInvalidator;
|
||||||
|
|
||||||
private static CacheMemoryAllocator _cacheAllocator;
|
private static CacheMemoryAllocator _cacheAllocator;
|
||||||
@@ -27,8 +26,6 @@ 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)]
|
||||||
@@ -48,9 +45,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var firstRegion = new ReservedRegion(allocator, CacheSize);
|
_jitRegion = new ReservedRegion(allocator, CacheSize);
|
||||||
_jitRegions.Add(firstRegion);
|
|
||||||
_activeRegionIndex = 0;
|
|
||||||
|
|
||||||
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
|
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
@@ -70,8 +65,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 = targetRegion.Pointer + funcOffset;
|
nint funcPtr = _jitRegion.Pointer + funcOffset;
|
||||||
|
|
||||||
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||||
{
|
{
|
||||||
@@ -85,11 +80,18 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ReprotectAsWritable(targetRegion, funcOffset, code.Length);
|
ReprotectAsWritable(funcOffset, code.Length);
|
||||||
Marshal.Copy(code.ToArray(), 0, funcPtr, code.Length);
|
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length));
|
||||||
ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
|
ReprotectAsExecutable(funcOffset, code.Length);
|
||||||
|
|
||||||
_jitCacheInvalidator?.Invalidate(funcPtr, (ulong)code.Length);
|
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||||
|
{
|
||||||
|
FlushInstructionCache(Process.GetCurrentProcess().Handle, funcPtr, (nuint)code.Length);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_jitCacheInvalidator?.Invalidate(funcPtr, (ulong)code.Length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Add(funcOffset, code.Length);
|
Add(funcOffset, code.Length);
|
||||||
@@ -104,80 +106,50 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
{
|
{
|
||||||
Debug.Assert(_initialized);
|
Debug.Assert(_initialized);
|
||||||
|
|
||||||
foreach (var region in _jitRegions)
|
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64());
|
||||||
|
|
||||||
|
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
|
||||||
{
|
{
|
||||||
if (pointer.ToInt64() < region.Pointer.ToInt64() ||
|
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
|
||||||
pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
|
_cacheEntries.RemoveAt(entryIndex);
|
||||||
{
|
|
||||||
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(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)
|
private static int Allocate(int codeSize)
|
||||||
{
|
{
|
||||||
codeSize = AlignCodeSize(codeSize);
|
codeSize = AlignCodeSize(codeSize);
|
||||||
|
|
||||||
for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
|
int allocOffset = _cacheAllocator.Allocate(codeSize);
|
||||||
|
|
||||||
|
if (allocOffset < 0)
|
||||||
{
|
{
|
||||||
int allocOffset = _cacheAllocator.Allocate(codeSize);
|
throw new OutOfMemoryException("JIT Cache exhausted.");
|
||||||
|
|
||||||
if (allocOffset >= 0)
|
|
||||||
{
|
|
||||||
_jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
|
||||||
_activeRegionIndex = i;
|
|
||||||
return allocOffset;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int exhaustedRegion = _activeRegionIndex;
|
_jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
||||||
var newRegion = new ReservedRegion(_jitRegions[0].Allocator, CacheSize);
|
|
||||||
_jitRegions.Add(newRegion);
|
|
||||||
_activeRegionIndex = _jitRegions.Count - 1;
|
|
||||||
|
|
||||||
int newRegionNumber = _activeRegionIndex;
|
|
||||||
|
|
||||||
Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {newRegionNumber} ({((newRegionNumber + 1) * CacheSize).Bytes()} Total Allocation).");
|
return allocOffset;
|
||||||
|
|
||||||
_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)
|
||||||
|
|||||||
@@ -150,7 +150,6 @@ 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 },
|
||||||
|
|||||||
@@ -126,14 +126,16 @@ namespace Ryujinx.Ava
|
|||||||
if (!TitleIDs.CurrentApplication.Value.HasValue) return;
|
if (!TitleIDs.CurrentApplication.Value.HasValue) return;
|
||||||
if (_discordPresencePlaying is null) return;
|
if (_discordPresencePlaying is null) return;
|
||||||
|
|
||||||
Analyzer.FormattedValue formattedValue =
|
FormattedValue formattedValue =
|
||||||
PlayReports.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
|
PlayReports.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
|
||||||
|
|
||||||
if (!formattedValue.Handled) return;
|
if (!formattedValue.Handled) return;
|
||||||
|
|
||||||
_discordPresencePlaying.Details = formattedValue.Reset
|
_discordPresencePlaying.Details = TruncateToByteLength(
|
||||||
? $"Playing {_currentApp.Title}"
|
formattedValue.Reset
|
||||||
: formattedValue.FormattedString;
|
? $"Playing {_currentApp.Title}"
|
||||||
|
: formattedValue.FormattedString
|
||||||
|
);
|
||||||
|
|
||||||
if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details))
|
if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details))
|
||||||
return; //don't trigger an update if the set presence Details are identical to current
|
return; //don't trigger an update if the set presence Details are identical to current
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs the configured <see cref="GameSpec.FormatterSpec"/> for the specified game title ID.
|
/// Runs the configured <see cref="GameSpec.FormatterSpec"/> for the specified game title ID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -98,261 +98,48 @@ namespace Ryujinx.Ava.Utilities.PlayReport
|
|||||||
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec))
|
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec))
|
||||||
return FormattedValue.Unhandled;
|
return FormattedValue.Unhandled;
|
||||||
|
|
||||||
foreach (GameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
|
foreach (FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
|
||||||
{
|
{
|
||||||
if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
|
if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
return formatSpec.ValueFormatter(new Value
|
return formatSpec.Formatter(new Value { Application = appMeta, PackedValue = valuePackObject });
|
||||||
{
|
|
||||||
Application = appMeta, PackedValue = valuePackObject
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (GameSpec.MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority))
|
foreach (MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority))
|
||||||
{
|
{
|
||||||
List<MessagePackObject> packedObjects = [];
|
List<MessagePackObject> packedObjects = [];
|
||||||
foreach (var reportKey in formatSpec.ReportKeys)
|
foreach (var reportKey in formatSpec.ReportKeys)
|
||||||
{
|
{
|
||||||
if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
|
if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
packedObjects.Add(valuePackObject);
|
packedObjects.Add(valuePackObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (packedObjects.Count != formatSpec.ReportKeys.Length)
|
if (packedObjects.Count != formatSpec.ReportKeys.Length)
|
||||||
return FormattedValue.Unhandled;
|
return FormattedValue.Unhandled;
|
||||||
|
|
||||||
return formatSpec.ValueFormatter(packedObjects
|
return formatSpec.Formatter(packedObjects
|
||||||
.Select(packObject => new Value { Application = appMeta, PackedValue = packObject })
|
.Select(packObject => new Value { Application = appMeta, PackedValue = packObject })
|
||||||
.ToArray());
|
.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (SparseMultiFormatterSpec formatSpec in spec.SparseMultiValueFormatters.OrderBy(x => x.Priority))
|
||||||
|
{
|
||||||
|
Dictionary<string, Value> packedObjects = [];
|
||||||
|
foreach (var reportKey in formatSpec.ReportKeys)
|
||||||
|
{
|
||||||
|
if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
packedObjects.Add(reportKey, new Value { Application = appMeta, PackedValue = valuePackObject });
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatSpec.Formatter(packedObjects);
|
||||||
|
}
|
||||||
|
|
||||||
return FormattedValue.Unhandled;
|
return FormattedValue.Unhandled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A potential formatted value returned by a <see cref="ValueFormatter"/>.
|
|
||||||
/// </summary>
|
|
||||||
public readonly struct FormattedValue
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Was any handler able to match anything in the Play Report?
|
|
||||||
/// </summary>
|
|
||||||
public bool Handled { get; private init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Did the handler request the caller of the <see cref="Analyzer"/> to reset the existing value?
|
|
||||||
/// </summary>
|
|
||||||
public bool Reset { get; private init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The formatted value, only present if <see cref="Handled"/> is true, and <see cref="Reset"/> is false.
|
|
||||||
/// </summary>
|
|
||||||
public string FormattedString { get; private init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The intended path of execution for having a string to return: simply return the string.
|
|
||||||
/// This implicit conversion will make the struct for you.<br/><br/>
|
|
||||||
///
|
|
||||||
/// If the input is null, <see cref="Unhandled"/> is returned.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="formattedValue">The formatted string value.</param>
|
|
||||||
/// <returns>The automatically constructed <see cref="FormattedValue"/> struct.</returns>
|
|
||||||
public static implicit operator FormattedValue(string formattedValue)
|
|
||||||
=> formattedValue is not null
|
|
||||||
? new FormattedValue { Handled = true, FormattedString = formattedValue }
|
|
||||||
: Unhandled;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Return this to tell the caller there is no value to return.
|
|
||||||
/// </summary>
|
|
||||||
public static FormattedValue Unhandled => default;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Return this to suggest the caller reset the value it's using the <see cref="Analyzer"/> for.
|
|
||||||
/// </summary>
|
|
||||||
public static FormattedValue ForceReset => new() { Handled = true, Reset = true };
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="ValueFormatter"/>.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly ValueFormatter AlwaysResets = _ => ForceReset;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A delegate factory you can use to always return the specified
|
|
||||||
/// <paramref name="formattedValue"/> in a <see cref="ValueFormatter"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
|
|
||||||
public static ValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A mapping of title IDs to value formatter specs.
|
|
||||||
///
|
|
||||||
/// <remarks>Generally speaking, use the <see cref="Analyzer"/>.AddSpec(...) methods instead of creating this class yourself.</remarks>
|
|
||||||
/// </summary>
|
|
||||||
public class GameSpec
|
|
||||||
{
|
|
||||||
public required string[] TitleIds { get; init; }
|
|
||||||
public List<FormatterSpec> SimpleValueFormatters { get; } = [];
|
|
||||||
public List<MultiFormatterSpec> MultiValueFormatters { get; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a value formatter to the current <see cref="GameSpec"/>
|
|
||||||
/// matching a specific key that could exist in a Play Report for the previously specified title IDs.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="reportKey">The key name to match.</param>
|
|
||||||
/// <param name="valueFormatter">The function which can return a potential formatted value.</param>
|
|
||||||
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
|
||||||
public GameSpec AddValueFormatter(string reportKey, ValueFormatter valueFormatter)
|
|
||||||
{
|
|
||||||
SimpleValueFormatters.Add(new FormatterSpec
|
|
||||||
{
|
|
||||||
Priority = SimpleValueFormatters.Count, ReportKey = reportKey, ValueFormatter = valueFormatter
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a value formatter at a specific priority to the current <see cref="GameSpec"/>
|
|
||||||
/// matching a specific key that could exist in a Play Report for the previously specified title IDs.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
|
||||||
/// <param name="reportKey">The key name to match.</param>
|
|
||||||
/// <param name="valueFormatter">The function which can return a potential formatted value.</param>
|
|
||||||
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
|
||||||
public GameSpec AddValueFormatter(int priority, string reportKey,
|
|
||||||
ValueFormatter valueFormatter)
|
|
||||||
{
|
|
||||||
SimpleValueFormatters.Add(new FormatterSpec
|
|
||||||
{
|
|
||||||
Priority = priority, ReportKey = reportKey, ValueFormatter = valueFormatter
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a multi-value formatter to the current <see cref="GameSpec"/>
|
|
||||||
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="reportKeys">The key names to match.</param>
|
|
||||||
/// <param name="valueFormatter">The function which can format the values.</param>
|
|
||||||
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
|
||||||
public GameSpec AddMultiValueFormatter(string[] reportKeys, MultiValueFormatter valueFormatter)
|
|
||||||
{
|
|
||||||
MultiValueFormatters.Add(new MultiFormatterSpec
|
|
||||||
{
|
|
||||||
Priority = SimpleValueFormatters.Count, ReportKeys = reportKeys, ValueFormatter = valueFormatter
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
|
|
||||||
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
|
||||||
/// <param name="reportKeys">The key names to match.</param>
|
|
||||||
/// <param name="valueFormatter">The function which can format the values.</param>
|
|
||||||
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
|
||||||
public GameSpec AddMultiValueFormatter(int priority, string[] reportKeys,
|
|
||||||
MultiValueFormatter valueFormatter)
|
|
||||||
{
|
|
||||||
MultiValueFormatters.Add(new MultiFormatterSpec
|
|
||||||
{
|
|
||||||
Priority = priority, ReportKeys = reportKeys, ValueFormatter = valueFormatter
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A struct containing the data for a mapping of a key in a Play Report to a formatter for its potential value.
|
|
||||||
/// </summary>
|
|
||||||
public struct FormatterSpec
|
|
||||||
{
|
|
||||||
public required int Priority { get; init; }
|
|
||||||
public required string ReportKey { get; init; }
|
|
||||||
public ValueFormatter ValueFormatter { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their potential values.
|
|
||||||
/// </summary>
|
|
||||||
public struct MultiFormatterSpec
|
|
||||||
{
|
|
||||||
public required int Priority { get; init; }
|
|
||||||
public required string[] ReportKeys { get; init; }
|
|
||||||
public MultiValueFormatter ValueFormatter { get; init; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The input data to a <see cref="ValueFormatter"/>,
|
|
||||||
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
|
|
||||||
/// and the matched <see cref="MessagePackObject"/> from the Play Report.
|
|
||||||
/// </summary>
|
|
||||||
public class Value
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The currently running application's <see cref="ApplicationMetadata"/>.
|
|
||||||
/// </summary>
|
|
||||||
public ApplicationMetadata Application { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The matched value from the Play Report.
|
|
||||||
/// </summary>
|
|
||||||
public MessagePackObject PackedValue { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Access the <see cref="PackedValue"/> as its underlying .NET type.<br/>
|
|
||||||
///
|
|
||||||
/// Does not seem to work well with comparing numeric types,
|
|
||||||
/// so use XValue properties for that.
|
|
||||||
/// </summary>
|
|
||||||
public object BoxedValue => PackedValue.ToObject();
|
|
||||||
|
|
||||||
#region AsX accessors
|
|
||||||
|
|
||||||
public bool BooleanValue => PackedValue.AsBoolean();
|
|
||||||
public byte ByteValye => PackedValue.AsByte();
|
|
||||||
public sbyte SByteValye => PackedValue.AsSByte();
|
|
||||||
public short ShortValye => PackedValue.AsInt16();
|
|
||||||
public ushort UShortValye => PackedValue.AsUInt16();
|
|
||||||
public int IntValye => PackedValue.AsInt32();
|
|
||||||
public uint UIntValye => PackedValue.AsUInt32();
|
|
||||||
public long LongValye => PackedValue.AsInt64();
|
|
||||||
public ulong ULongValye => PackedValue.AsUInt64();
|
|
||||||
public float FloatValue => PackedValue.AsSingle();
|
|
||||||
public double DoubleValue => PackedValue.AsDouble();
|
|
||||||
public string StringValue => PackedValue.AsString();
|
|
||||||
public Span<byte> BinaryValue => PackedValue.AsBinary();
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The delegate type that powers single value formatters.<br/>
|
|
||||||
/// Takes in the result value from the Play Report, and outputs:
|
|
||||||
/// <br/>
|
|
||||||
/// a formatted string,
|
|
||||||
/// <br/>
|
|
||||||
/// a signal that nothing was available to handle it,
|
|
||||||
/// <br/>
|
|
||||||
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
|
|
||||||
/// </summary>
|
|
||||||
public delegate Analyzer.FormattedValue ValueFormatter(Value value);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The delegate type that powers multiple value formatters.<br/>
|
|
||||||
/// Takes in the result value from the Play Report, and outputs:
|
|
||||||
/// <br/>
|
|
||||||
/// a formatted string,
|
|
||||||
/// <br/>
|
|
||||||
/// a signal that nothing was available to handle it,
|
|
||||||
/// <br/>
|
|
||||||
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
|
|
||||||
/// </summary>
|
|
||||||
public delegate Analyzer.FormattedValue MultiValueFormatter(Value[] value);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Utilities.PlayReport
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The delegate type that powers single value formatters.<br/>
|
||||||
|
/// Takes in the result value from the Play Report, and outputs:
|
||||||
|
/// <br/>
|
||||||
|
/// a formatted string,
|
||||||
|
/// <br/>
|
||||||
|
/// a signal that nothing was available to handle it,
|
||||||
|
/// <br/>
|
||||||
|
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
|
||||||
|
/// </summary>
|
||||||
|
public delegate FormattedValue ValueFormatter(Value value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The delegate type that powers multiple value formatters.<br/>
|
||||||
|
/// Takes in the result values from the Play Report, and outputs:
|
||||||
|
/// <br/>
|
||||||
|
/// a formatted string,
|
||||||
|
/// <br/>
|
||||||
|
/// a signal that nothing was available to handle it,
|
||||||
|
/// <br/>
|
||||||
|
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
|
||||||
|
/// </summary>
|
||||||
|
public delegate FormattedValue MultiValueFormatter(Value[] value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The delegate type that powers multiple value formatters.
|
||||||
|
/// The dictionary passed to this delegate is sparsely populated;
|
||||||
|
/// that is, not every key specified in the Play Report needs to match for this to be used.<br/>
|
||||||
|
/// Takes in the result values from the Play Report, and outputs:
|
||||||
|
/// <br/>
|
||||||
|
/// a formatted string,
|
||||||
|
/// <br/>
|
||||||
|
/// a signal that nothing was available to handle it,
|
||||||
|
/// <br/>
|
||||||
|
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
|
||||||
|
/// </summary>
|
||||||
|
public delegate FormattedValue SparseMultiValueFormatter(Dictionary<string, Value> values);
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using static Ryujinx.Ava.Utilities.PlayReport.Analyzer;
|
namespace Ryujinx.Ava.Utilities.PlayReport
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Utilities.PlayReport
|
|
||||||
{
|
{
|
||||||
public static class PlayReports
|
public static class PlayReports
|
||||||
{
|
{
|
||||||
@@ -10,7 +8,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
|
|||||||
spec => spec
|
spec => spec
|
||||||
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
|
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
|
||||||
// reset to normal status when switching between normal & master mode in title screen
|
// reset to normal status when switching between normal & master mode in title screen
|
||||||
.AddValueFormatter("AoCVer", FormattedValue.AlwaysResets)
|
.AddValueFormatter("AoCVer", FormattedValue.SingleAlwaysResets)
|
||||||
)
|
)
|
||||||
.AddSpec(
|
.AddSpec(
|
||||||
"0100f2c0115b6000",
|
"0100f2c0115b6000",
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
using FluentAvalonia.Core;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Utilities.PlayReport
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A mapping of title IDs to value formatter specs.
|
||||||
|
///
|
||||||
|
/// <remarks>Generally speaking, use the <see cref="Analyzer"/>.AddSpec(...) methods instead of creating this class yourself.</remarks>
|
||||||
|
/// </summary>
|
||||||
|
public class GameSpec
|
||||||
|
{
|
||||||
|
public required string[] TitleIds { get; init; }
|
||||||
|
public List<FormatterSpec> SimpleValueFormatters { get; } = [];
|
||||||
|
public List<MultiFormatterSpec> MultiValueFormatters { get; } = [];
|
||||||
|
public List<SparseMultiFormatterSpec> SparseMultiValueFormatters { get; } = [];
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a value formatter to the current <see cref="GameSpec"/>
|
||||||
|
/// matching a specific key that could exist in a Play Report for the previously specified title IDs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reportKey">The key name to match.</param>
|
||||||
|
/// <param name="valueFormatter">The function which can return a potential formatted value.</param>
|
||||||
|
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||||
|
public GameSpec AddValueFormatter(string reportKey, ValueFormatter valueFormatter)
|
||||||
|
=> AddValueFormatter(SimpleValueFormatters.Count, reportKey, valueFormatter);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a value formatter at a specific priority to the current <see cref="GameSpec"/>
|
||||||
|
/// matching a specific key that could exist in a Play Report for the previously specified title IDs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
||||||
|
/// <param name="reportKey">The key name to match.</param>
|
||||||
|
/// <param name="valueFormatter">The function which can return a potential formatted value.</param>
|
||||||
|
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||||
|
public GameSpec AddValueFormatter(int priority, string reportKey,
|
||||||
|
ValueFormatter valueFormatter)
|
||||||
|
{
|
||||||
|
SimpleValueFormatters.Add(new FormatterSpec
|
||||||
|
{
|
||||||
|
Priority = priority, ReportKey = reportKey, Formatter = valueFormatter
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a multi-value formatter to the current <see cref="GameSpec"/>
|
||||||
|
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reportKeys">The key names to match.</param>
|
||||||
|
/// <param name="valueFormatter">The function which can format the values.</param>
|
||||||
|
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||||
|
public GameSpec AddMultiValueFormatter(string[] reportKeys, MultiValueFormatter valueFormatter)
|
||||||
|
=> AddMultiValueFormatter(MultiValueFormatters.Count, reportKeys, valueFormatter);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
|
||||||
|
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
||||||
|
/// <param name="reportKeys">The key names to match.</param>
|
||||||
|
/// <param name="valueFormatter">The function which can format the values.</param>
|
||||||
|
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||||
|
public GameSpec AddMultiValueFormatter(int priority, string[] reportKeys,
|
||||||
|
MultiValueFormatter valueFormatter)
|
||||||
|
{
|
||||||
|
MultiValueFormatters.Add(new MultiFormatterSpec
|
||||||
|
{
|
||||||
|
Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a multi-value formatter to the current <see cref="GameSpec"/>
|
||||||
|
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
||||||
|
/// <br/><br/>
|
||||||
|
/// The 'Sparse' multi-value formatters do not require every key to be present.
|
||||||
|
/// If you need this requirement, use <see cref="AddMultiValueFormatter(string[], Ryujinx.Ava.Utilities.PlayReport.MultiValueFormatter)"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reportKeys">The key names to match.</param>
|
||||||
|
/// <param name="valueFormatter">The function which can format the values.</param>
|
||||||
|
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||||
|
public GameSpec AddSparseMultiValueFormatter(string[] reportKeys, SparseMultiValueFormatter valueFormatter)
|
||||||
|
=> AddSparseMultiValueFormatter(SparseMultiValueFormatters.Count, reportKeys, valueFormatter);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
|
||||||
|
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
||||||
|
/// <br/><br/>
|
||||||
|
/// The 'Sparse' multi-value formatters do not require every key to be present.
|
||||||
|
/// If you need this requirement, use <see cref="AddMultiValueFormatter(int, string[], Ryujinx.Ava.Utilities.PlayReport.MultiValueFormatter)"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
||||||
|
/// <param name="reportKeys">The key names to match.</param>
|
||||||
|
/// <param name="valueFormatter">The function which can format the values.</param>
|
||||||
|
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||||
|
public GameSpec AddSparseMultiValueFormatter(int priority, string[] reportKeys,
|
||||||
|
SparseMultiValueFormatter valueFormatter)
|
||||||
|
{
|
||||||
|
SparseMultiValueFormatters.Add(new SparseMultiFormatterSpec
|
||||||
|
{
|
||||||
|
Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A struct containing the data for a mapping of a key in a Play Report to a formatter for its potential value.
|
||||||
|
/// </summary>
|
||||||
|
public struct FormatterSpec
|
||||||
|
{
|
||||||
|
public required int Priority { get; init; }
|
||||||
|
public required string ReportKey { get; init; }
|
||||||
|
public ValueFormatter Formatter { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their potential values.
|
||||||
|
/// </summary>
|
||||||
|
public struct MultiFormatterSpec
|
||||||
|
{
|
||||||
|
public required int Priority { get; init; }
|
||||||
|
public required string[] ReportKeys { get; init; }
|
||||||
|
public MultiValueFormatter Formatter { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their sparsely populated potential values.
|
||||||
|
/// </summary>
|
||||||
|
public struct SparseMultiFormatterSpec
|
||||||
|
{
|
||||||
|
public required int Priority { get; init; }
|
||||||
|
public required string[] ReportKeys { get; init; }
|
||||||
|
public SparseMultiValueFormatter Formatter { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
using MsgPack;
|
||||||
|
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Utilities.PlayReport
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The input data to a <see cref="ValueFormatter"/>,
|
||||||
|
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
|
||||||
|
/// and the matched <see cref="MessagePackObject"/> from the Play Report.
|
||||||
|
/// </summary>
|
||||||
|
public class Value
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The currently running application's <see cref="ApplicationMetadata"/>.
|
||||||
|
/// </summary>
|
||||||
|
public ApplicationMetadata Application { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The matched value from the Play Report.
|
||||||
|
/// </summary>
|
||||||
|
public MessagePackObject PackedValue { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access the <see cref="PackedValue"/> as its underlying .NET type.<br/>
|
||||||
|
///
|
||||||
|
/// Does not seem to work well with comparing numeric types,
|
||||||
|
/// so use XValue properties for that.
|
||||||
|
/// </summary>
|
||||||
|
public object BoxedValue => PackedValue.ToObject();
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
object boxed = BoxedValue;
|
||||||
|
return boxed == null
|
||||||
|
? "null"
|
||||||
|
: boxed.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region AsX accessors
|
||||||
|
|
||||||
|
public bool BooleanValue => PackedValue.AsBoolean();
|
||||||
|
public byte ByteValue => PackedValue.AsByte();
|
||||||
|
public sbyte SByteValue => PackedValue.AsSByte();
|
||||||
|
public short ShortValue => PackedValue.AsInt16();
|
||||||
|
public ushort UShortValue => PackedValue.AsUInt16();
|
||||||
|
public int IntValue => PackedValue.AsInt32();
|
||||||
|
public uint UIntValue => PackedValue.AsUInt32();
|
||||||
|
public long LongValue => PackedValue.AsInt64();
|
||||||
|
public ulong ULongValue => PackedValue.AsUInt64();
|
||||||
|
public float FloatValue => PackedValue.AsSingle();
|
||||||
|
public double DoubleValue => PackedValue.AsDouble();
|
||||||
|
public string StringValue => PackedValue.AsString();
|
||||||
|
public Span<byte> BinaryValue => PackedValue.AsBinary();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A potential formatted value returned by a <see cref="ValueFormatter"/>.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct FormattedValue
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Was any handler able to match anything in the Play Report?
|
||||||
|
/// </summary>
|
||||||
|
public bool Handled { get; private init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Did the handler request the caller of the <see cref="Analyzer"/> to reset the existing value?
|
||||||
|
/// </summary>
|
||||||
|
public bool Reset { get; private init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The formatted value, only present if <see cref="Handled"/> is true, and <see cref="Reset"/> is false.
|
||||||
|
/// </summary>
|
||||||
|
public string FormattedString { get; private init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The intended path of execution for having a string to return: simply return the string.
|
||||||
|
/// This implicit conversion will make the struct for you.<br/><br/>
|
||||||
|
///
|
||||||
|
/// If the input is null, <see cref="Unhandled"/> is returned.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="formattedValue">The formatted string value.</param>
|
||||||
|
/// <returns>The automatically constructed <see cref="FormattedValue"/> struct.</returns>
|
||||||
|
public static implicit operator FormattedValue(string formattedValue)
|
||||||
|
=> formattedValue is not null
|
||||||
|
? new FormattedValue { Handled = true, FormattedString = formattedValue }
|
||||||
|
: Unhandled;
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
if (!Handled)
|
||||||
|
return "<Unhandled>";
|
||||||
|
|
||||||
|
if (Reset)
|
||||||
|
return "<Reset>";
|
||||||
|
|
||||||
|
return FormattedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return this to tell the caller there is no value to return.
|
||||||
|
/// </summary>
|
||||||
|
public static FormattedValue Unhandled => default;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return this to suggest the caller reset the value it's using the <see cref="Analyzer"/> for.
|
||||||
|
/// </summary>
|
||||||
|
public static FormattedValue ForceReset => new() { Handled = true, Reset = true };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="ValueFormatter"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly ValueFormatter SingleAlwaysResets = _ => ForceReset;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="MultiValueFormatter"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly MultiValueFormatter MultiAlwaysResets = _ => ForceReset;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A delegate factory you can use to always return the specified
|
||||||
|
/// <paramref name="formattedValue"/> in a <see cref="ValueFormatter"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
|
||||||
|
public static ValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user