Compare commits

..

3 Commits

Author SHA1 Message Date
Evan Husted 1d88771d1b Play Report Analyzer v4
You can now access the *entire* play report data in any given value formatter.
The input types have been restructured and, notably, not every instance of Value has an ApplicationMetadata on it. It's now on the container type that also contains the matched values and the entire play report.
2025-02-08 00:22:34 -06:00
Evan Husted 4e8157688e UI: See what games do/don't have an image & dynamic RPC support in the Game Info popup 2025-02-07 18:34:11 -06:00
Evan Husted 5085af0050 UI: Changed the color of "Ingame" from yellow to orange to stand out better in light mode 2025-02-07 18:28:32 -06:00
17 changed files with 367 additions and 195 deletions
-2
View File
@@ -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;
+35 -77
View File
@@ -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;
}
} }
} }
+33 -61
View File
@@ -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 },
+3 -2
View File
@@ -1,5 +1,6 @@
using MsgPack; using MsgPack;
using Ryujinx.Horizon.Common; using Ryujinx.Horizon.Common;
using Ryujinx.Horizon.Prepo.Types;
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
using System.Threading; using System.Threading;
@@ -8,7 +9,7 @@ namespace Ryujinx.Horizon
{ {
public static class HorizonStatic public static class HorizonStatic
{ {
internal static void HandlePlayReport(MessagePackObject report) => internal static void HandlePlayReport(PlayReport report) =>
new Thread(() => PlayReport?.Invoke(report)) new Thread(() => PlayReport?.Invoke(report))
{ {
Name = "HLE.PlayReportEvent", Name = "HLE.PlayReportEvent",
@@ -16,7 +17,7 @@ namespace Ryujinx.Horizon
Priority = ThreadPriority.AboveNormal Priority = ThreadPriority.AboveNormal
}.Start(); }.Start();
public static event Action<MessagePackObject> PlayReport; public static event Action<PlayReport> PlayReport;
[field: ThreadStatic] [field: ThreadStatic]
public static HorizonOptions Options { get; private set; } public static HorizonOptions Options { get; private set; }
+14 -10
View File
@@ -1,4 +1,3 @@
using Gommon;
using MsgPack; using MsgPack;
using MsgPack.Serialization; using MsgPack.Serialization;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
@@ -12,19 +11,12 @@ using Ryujinx.Horizon.Sdk.Sf;
using Ryujinx.Horizon.Sdk.Sf.Hipc; using Ryujinx.Horizon.Sdk.Sf.Hipc;
using System; using System;
using System.Text; using System.Text;
using System.Threading;
using ApplicationId = Ryujinx.Horizon.Sdk.Ncm.ApplicationId; using ApplicationId = Ryujinx.Horizon.Sdk.Ncm.ApplicationId;
namespace Ryujinx.Horizon.Prepo.Ipc namespace Ryujinx.Horizon.Prepo.Ipc
{ {
partial class PrepoService : IPrepoService partial class PrepoService : IPrepoService
{ {
enum PlayReportKind
{
Normal,
System,
}
private readonly ArpApi _arp; private readonly ArpApi _arp;
private readonly PrepoServicePermissionLevel _permissionLevel; private readonly PrepoServicePermissionLevel _permissionLevel;
private ulong _systemSessionId; private ulong _systemSessionId;
@@ -196,10 +188,17 @@ namespace Ryujinx.Horizon.Prepo.Ipc
{ {
return PrepoResult.InvalidBufferSize; return PrepoResult.InvalidBufferSize;
} }
StringBuilder builder = new(); StringBuilder builder = new();
MessagePackObject deserializedReport = MessagePackSerializer.UnpackMessagePackObject(reportBuffer.ToArray()); MessagePackObject deserializedReport = MessagePackSerializer.UnpackMessagePackObject(reportBuffer.ToArray());
PlayReport playReport = new()
{
Kind = playReportKind,
Room = gameRoom,
ReportData = deserializedReport
};
builder.AppendLine(); builder.AppendLine();
builder.AppendLine("PlayReport log:"); builder.AppendLine("PlayReport log:");
builder.AppendLine($" Kind: {playReportKind}"); builder.AppendLine($" Kind: {playReportKind}");
@@ -209,10 +208,12 @@ namespace Ryujinx.Horizon.Prepo.Ipc
if (pid != 0) if (pid != 0)
{ {
builder.AppendLine($" Pid: {pid}"); builder.AppendLine($" Pid: {pid}");
playReport.Pid = pid;
} }
else else
{ {
builder.AppendLine($" ApplicationId: {applicationId}"); builder.AppendLine($" ApplicationId: {applicationId}");
playReport.AppId = applicationId;
} }
Result result = _arp.GetApplicationInstanceId(out ulong applicationInstanceId, pid); Result result = _arp.GetApplicationInstanceId(out ulong applicationInstanceId, pid);
@@ -223,17 +224,20 @@ namespace Ryujinx.Horizon.Prepo.Ipc
_arp.GetApplicationLaunchProperty(out ApplicationLaunchProperty applicationLaunchProperty, applicationInstanceId).AbortOnFailure(); _arp.GetApplicationLaunchProperty(out ApplicationLaunchProperty applicationLaunchProperty, applicationInstanceId).AbortOnFailure();
playReport.Version = applicationLaunchProperty.Version;
builder.AppendLine($" ApplicationVersion: {applicationLaunchProperty.Version}"); builder.AppendLine($" ApplicationVersion: {applicationLaunchProperty.Version}");
if (!userId.IsNull) if (!userId.IsNull)
{ {
builder.AppendLine($" UserId: {userId}"); builder.AppendLine($" UserId: {userId}");
playReport.UserId = userId;
} }
builder.AppendLine($" Room: {gameRoom}"); builder.AppendLine($" Room: {gameRoom}");
builder.AppendLine($" Report: {MessagePackObjectFormatter.Format(deserializedReport)}"); builder.AppendLine($" Report: {MessagePackObjectFormatter.Format(deserializedReport)}");
HorizonStatic.HandlePlayReport(deserializedReport); HorizonStatic.HandlePlayReport(playReport);
Logger.Info?.Print(LogClass.ServicePrepo, builder.ToString()); Logger.Info?.Print(LogClass.ServicePrepo, builder.ToString());
@@ -0,0 +1,24 @@
using MsgPack;
using Ryujinx.Horizon.Sdk.Account;
using Ryujinx.Horizon.Sdk.Ncm;
namespace Ryujinx.Horizon.Prepo.Types
{
public struct PlayReport
{
public PlayReportKind Kind { get; init; }
public string Room { get; init; }
public MessagePackObject ReportData { get; init; }
public ApplicationId? AppId;
public ulong? Pid;
public uint Version;
public Uid? UserId;
}
public enum PlayReportKind
{
Normal,
System,
}
}
+50
View File
@@ -23696,6 +23696,56 @@
"zh_CN": "选择一个要解压的 DLC", "zh_CN": "选择一个要解压的 DLC",
"zh_TW": "" "zh_TW": ""
} }
},
{
"ID": "GameInfoRpcImage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Rich Presence Image",
"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": "GameInfoRpcDynamic",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Dynamic Rich Presence",
"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": ""
}
} }
] ]
} }
+6 -1
View File
@@ -10,6 +10,8 @@ using Ryujinx.Common.Logging;
using Ryujinx.HLE; 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 System.Linq;
using System.Text; using System.Text;
namespace Ryujinx.Ava namespace Ryujinx.Ava
@@ -37,6 +39,9 @@ namespace Ryujinx.Ava
private static RichPresence _discordPresencePlaying; private static RichPresence _discordPresencePlaying;
private static ApplicationMetadata _currentApp; private static ApplicationMetadata _currentApp;
public static bool HasAssetImage(string titleId) => TitleIDs.DiscordGameAssetKeys.ContainsIgnoreCase(titleId);
public static bool HasAnalyzer(string titleId) => PlayReports.Analyzer.TitleIds.ContainsIgnoreCase(titleId);
public static void Initialize() public static void Initialize()
{ {
_discordPresenceMain = new RichPresence _discordPresenceMain = new RichPresence
@@ -120,7 +125,7 @@ namespace Ryujinx.Ava
_currentApp = null; _currentApp = null;
} }
private static void HandlePlayReport(MessagePackObject playReport) private static void HandlePlayReport(PlayReport playReport)
{ {
if (_discordClient is null) return; if (_discordClient is null) return;
if (!TitleIDs.CurrentApplication.Value.HasValue) return; if (!TitleIDs.CurrentApplication.Value.HasValue) return;
@@ -4,6 +4,7 @@
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:ext="using:Ryujinx.Ava.Common.Markup" xmlns:ext="using:Ryujinx.Ava.Common.Markup"
xmlns:viewModels="using:Ryujinx.Ava.UI.ViewModels" xmlns:viewModels="using:Ryujinx.Ava.UI.ViewModels"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.UI.Controls.ApplicationDataView" x:Class="Ryujinx.Ava.UI.Controls.ApplicationDataView"
@@ -85,6 +86,49 @@
</StackPanel> </StackPanel>
</Grid> </Grid>
<Separator Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" /> <Separator Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
<StackPanel Orientation="Vertical" Spacing="5">
<StackPanel Orientation="Horizontal" Spacing="5">
<ui:SymbolIcon Foreground="ForestGreen" Symbol="Checkmark" IsVisible="{Binding AppData.HasRichPresenceAsset}"/>
<TextBlock
Foreground="ForestGreen"
HorizontalAlignment="Stretch"
IsVisible="{Binding AppData.HasRichPresenceAsset}"
Text="{ext:Locale GameInfoRpcImage}"
TextAlignment="Start"
TextWrapping="Wrap" >
</TextBlock>
<ui:SymbolIcon Foreground="Red" Symbol="Cancel" IsVisible="{Binding !AppData.HasRichPresenceAsset}"/>
<TextBlock
Foreground="Red"
HorizontalAlignment="Stretch"
IsVisible="{Binding !AppData.HasRichPresenceAsset}"
Text="{ext:Locale GameInfoRpcImage}"
TextAlignment="Start"
TextWrapping="Wrap" >
</TextBlock>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="5">
<ui:SymbolIcon Foreground="ForestGreen" Symbol="Checkmark" IsVisible="{Binding AppData.HasDynamicRichPresenceSupport}"/>
<TextBlock
Foreground="ForestGreen"
HorizontalAlignment="Stretch"
IsVisible="{Binding AppData.HasDynamicRichPresenceSupport}"
Text="{ext:Locale GameInfoRpcDynamic}"
TextAlignment="Start"
TextWrapping="Wrap" >
</TextBlock>
<ui:SymbolIcon Foreground="Red" Symbol="Cancel" IsVisible="{Binding !AppData.HasDynamicRichPresenceSupport}"/>
<TextBlock
Foreground="Red"
HorizontalAlignment="Stretch"
IsVisible="{Binding !AppData.HasDynamicRichPresenceSupport}"
Text="{ext:Locale GameInfoRpcDynamic}"
TextAlignment="Start"
TextWrapping="Wrap" >
</TextBlock>
</StackPanel>
</StackPanel>
<Separator Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
<TextBlock <TextBlock
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
IsVisible="{Binding AppData.HasLdnGames}" IsVisible="{Binding AppData.HasLdnGames}"
@@ -18,7 +18,7 @@ namespace Ryujinx.Ava.UI.Helpers
LocaleKeys.CompatibilityListNothing or LocaleKeys.CompatibilityListNothing or
LocaleKeys.CompatibilityListBoots or LocaleKeys.CompatibilityListBoots or
LocaleKeys.CompatibilityListMenus => Brushes.Red, LocaleKeys.CompatibilityListMenus => Brushes.Red,
LocaleKeys.CompatibilityListIngame => Brushes.Yellow, LocaleKeys.CompatibilityListIngame => Brushes.DarkOrange,
_ => Brushes.ForestGreen _ => Brushes.ForestGreen
}; };
@@ -63,6 +63,9 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public int GameCount { get; set; } public int GameCount { get; set; }
public bool HasLdnGames => PlayerCount != 0 && GameCount != 0; public bool HasLdnGames => PlayerCount != 0 && GameCount != 0;
public bool HasRichPresenceAsset => DiscordIntegrationModule.HasAssetImage(IdString);
public bool HasDynamicRichPresenceSupport => DiscordIntegrationModule.HasAnalyzer(IdString);
public TimeSpan TimePlayed { get; set; } public TimeSpan TimePlayed { get; set; }
public DateTime? LastPlayed { get; set; } public DateTime? LastPlayed { get; set; }
+29 -13
View File
@@ -3,6 +3,7 @@ using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.AppLibrary;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@@ -15,6 +16,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport
{ {
private readonly List<GameSpec> _specs = []; private readonly List<GameSpec> _specs = [];
public string[] TitleIds => Specs.SelectMany(x => x.TitleIds).ToArray();
public IReadOnlyList<GameSpec> Specs => new ReadOnlyCollection<GameSpec>(_specs);
/// <summary> /// <summary>
/// Add an analysis spec matching a specific game by title ID, with the provided spec configuration. /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
/// </summary> /// </summary>
@@ -80,7 +85,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <summary> /// <summary>
/// Runs the configured <see cref="GameSpec.FormatterSpec"/> for the specified game title ID. /// Runs the configured <see cref="FormatterSpec"/> for the specified game title ID.
/// </summary> /// </summary>
/// <param name="runningGameId">The game currently running.</param> /// <param name="runningGameId">The game currently running.</param>
/// <param name="appMeta">The Application metadata information, including localized game name and play time information.</param> /// <param name="appMeta">The Application metadata information, including localized game name and play time information.</param>
@@ -89,10 +94,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport
public FormattedValue Format( public FormattedValue Format(
string runningGameId, string runningGameId,
ApplicationMetadata appMeta, ApplicationMetadata appMeta,
MessagePackObject playReport Horizon.Prepo.Types.PlayReport playReport
) )
{ {
if (!playReport.IsDictionary) if (!playReport.ReportData.IsDictionary)
return FormattedValue.Unhandled; return FormattedValue.Unhandled;
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec)) if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec))
@@ -100,10 +105,14 @@ namespace Ryujinx.Ava.Utilities.PlayReport
foreach (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.ReportData.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
continue; continue;
return formatSpec.Formatter(new Value { Application = appMeta, PackedValue = valuePackObject }); return formatSpec.Formatter(new SingleValue(valuePackObject)
{
Application = appMeta,
PlayReport = playReport
});
} }
foreach (MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority)) foreach (MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority))
@@ -111,7 +120,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
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.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
continue; continue;
packedObjects.Add(valuePackObject); packedObjects.Add(valuePackObject);
@@ -120,23 +129,30 @@ namespace Ryujinx.Ava.Utilities.PlayReport
if (packedObjects.Count != formatSpec.ReportKeys.Length) if (packedObjects.Count != formatSpec.ReportKeys.Length)
return FormattedValue.Unhandled; return FormattedValue.Unhandled;
return formatSpec.Formatter(packedObjects return formatSpec.Formatter(new MultiValue(packedObjects)
.Select(packObject => new Value { Application = appMeta, PackedValue = packObject }) {
.ToArray()); Application = appMeta,
PlayReport = playReport
});
} }
foreach (SparseMultiFormatterSpec formatSpec in spec.SparseMultiValueFormatters.OrderBy(x => x.Priority)) foreach (SparseMultiFormatterSpec formatSpec in spec.SparseMultiValueFormatters.OrderBy(x => x.Priority))
{ {
Dictionary<string, Value> packedObjects = []; Dictionary<string, MessagePackObject> packedObjects = [];
foreach (var reportKey in formatSpec.ReportKeys) foreach (var reportKey in formatSpec.ReportKeys)
{ {
if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
continue; continue;
packedObjects.Add(reportKey, new Value { Application = appMeta, PackedValue = valuePackObject }); packedObjects.Add(reportKey, valuePackObject);
} }
return formatSpec.Formatter(packedObjects); return formatSpec.Formatter(
new SparseMultiValue(packedObjects)
{
Application = appMeta,
PlayReport = playReport
});
} }
return FormattedValue.Unhandled; return FormattedValue.Unhandled;
@@ -1,6 +1,4 @@
using System.Collections.Generic; namespace Ryujinx.Ava.Utilities.PlayReport
namespace Ryujinx.Ava.Utilities.PlayReport
{ {
/// <summary> /// <summary>
/// The delegate type that powers single value formatters.<br/> /// The delegate type that powers single value formatters.<br/>
@@ -12,7 +10,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <br/> /// <br/>
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for. /// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
/// </summary> /// </summary>
public delegate FormattedValue ValueFormatter(Value value); public delegate FormattedValue ValueFormatter(SingleValue value);
/// <summary> /// <summary>
/// The delegate type that powers multiple value formatters.<br/> /// The delegate type that powers multiple value formatters.<br/>
@@ -24,7 +22,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <br/> /// <br/>
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for. /// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
/// </summary> /// </summary>
public delegate FormattedValue MultiValueFormatter(Value[] value); public delegate FormattedValue MultiValueFormatter(MultiValue value);
/// <summary> /// <summary>
/// The delegate type that powers multiple value formatters. /// The delegate type that powers multiple value formatters.
@@ -38,5 +36,5 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <br/> /// <br/>
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for. /// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
/// </summary> /// </summary>
public delegate FormattedValue SparseMultiValueFormatter(Dictionary<string, Value> values); public delegate FormattedValue SparseMultiValueFormatter(SparseMultiValue value);
} }
@@ -0,0 +1,87 @@
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Utilities.PlayReport
{
public abstract class MatchedValue<T>
{
public MatchedValue(T matched)
{
Matched = matched;
}
/// <summary>
/// The currently running application's <see cref="ApplicationMetadata"/>.
/// </summary>
public ApplicationMetadata Application { get; init; }
/// <summary>
/// The entire play report.
/// </summary>
public Horizon.Prepo.Types.PlayReport PlayReport { get; init; }
/// <summary>
/// The matched value from the Play Report.
/// </summary>
public T Matched { 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 SingleValue : MatchedValue<Value>
{
public SingleValue(Value matched) : base(matched)
{
}
public static implicit operator SingleValue(MessagePackObject mpo) => new(mpo);
}
/// <summary>
/// The input data to a <see cref="MultiValueFormatter"/>,
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
/// and the matched <see cref="MessagePackObject"/>s from the Play Report.
/// </summary>
public class MultiValue : MatchedValue<Value[]>
{
public MultiValue(Value[] matched) : base(matched)
{
}
public MultiValue(IEnumerable<MessagePackObject> matched) : base(Value.ConvertPackedObjects(matched))
{
}
public static implicit operator MultiValue(List<MessagePackObject> matched)
=> new(matched.Select(x => new Value(x)).ToArray());
}
/// <summary>
/// The input data to a <see cref="SparseMultiValueFormatter"/>,
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
/// and the matched <see cref="MessagePackObject"/>s from the Play Report.
/// </summary>
public class SparseMultiValue : MatchedValue<Dictionary<string, Value>>
{
public SparseMultiValue(Dictionary<string, Value> matched) : base(matched)
{
}
public SparseMultiValue(Dictionary<string, MessagePackObject> matched) : base(Value.ConvertPackedObjectMap(matched))
{
}
public static implicit operator SparseMultiValue(Dictionary<string, MessagePackObject> matched)
=> new(matched
.ToDictionary(
x => x.Key,
x => new Value(x.Value)
)
);
}
}
+16 -16
View File
@@ -39,28 +39,28 @@
.AddValueFormatter("team_circle", PokemonSVUnionCircle) .AddValueFormatter("team_circle", PokemonSVUnionCircle)
); );
private static FormattedValue BreathOfTheWild_MasterMode(Value value) private static FormattedValue BreathOfTheWild_MasterMode(SingleValue value)
=> value.BoxedValue is 1 ? "Playing Master Mode" : FormattedValue.ForceReset; => value.Matched.BoxedValue is 1 ? "Playing Master Mode" : FormattedValue.ForceReset;
private static FormattedValue TearsOfTheKingdom_CurrentField(Value value) => private static FormattedValue TearsOfTheKingdom_CurrentField(SingleValue value) =>
value.DoubleValue switch value.Matched.DoubleValue switch
{ {
> 800d => "Exploring the Sky Islands", > 800d => "Exploring the Sky Islands",
< -201d => "Exploring the Depths", < -201d => "Exploring the Depths",
_ => "Roaming Hyrule" _ => "Roaming Hyrule"
}; };
private static FormattedValue SuperMarioOdyssey_AssistMode(Value value) private static FormattedValue SuperMarioOdyssey_AssistMode(SingleValue value)
=> value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode"; => value.Matched.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
private static FormattedValue SuperMarioOdysseyChina_AssistMode(Value value) private static FormattedValue SuperMarioOdysseyChina_AssistMode(SingleValue value)
=> value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式"; => value.Matched.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
private static FormattedValue SuperMario3DWorldOrBowsersFury(Value value) private static FormattedValue SuperMario3DWorldOrBowsersFury(SingleValue value)
=> value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury"; => value.Matched.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
private static FormattedValue MarioKart8Deluxe_Mode(Value value) private static FormattedValue MarioKart8Deluxe_Mode(SingleValue value)
=> value.StringValue switch => value.Matched.StringValue switch
{ {
// Single Player // Single Player
"Single" => "Single Player", "Single" => "Single Player",
@@ -87,11 +87,11 @@
_ => FormattedValue.ForceReset _ => FormattedValue.ForceReset
}; };
private static FormattedValue PokemonSVUnionCircle(Value value) private static FormattedValue PokemonSVUnionCircle(SingleValue value)
=> value.BoxedValue is 0 ? "Playing Alone" : "Playing in a group"; => value.Matched.BoxedValue is 0 ? "Playing Alone" : "Playing in a group";
private static FormattedValue PokemonSVArea(Value value) private static FormattedValue PokemonSVArea(SingleValue value)
=> value.StringValue switch => value.Matched.StringValue switch
{ {
// Base Game Locations // Base Game Locations
"a_w01" => "South Area One", "a_w01" => "South Area One",
+18 -5
View File
@@ -1,6 +1,8 @@
using MsgPack; using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.AppLibrary;
using System; using System;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Utilities.PlayReport namespace Ryujinx.Ava.Utilities.PlayReport
{ {
@@ -9,12 +11,12 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// containing the currently running application's <see cref="ApplicationMetadata"/>, /// containing the currently running application's <see cref="ApplicationMetadata"/>,
/// and the matched <see cref="MessagePackObject"/> from the Play Report. /// and the matched <see cref="MessagePackObject"/> from the Play Report.
/// </summary> /// </summary>
public class Value public readonly struct Value
{ {
/// <summary> public Value(MessagePackObject packedValue)
/// The currently running application's <see cref="ApplicationMetadata"/>. {
/// </summary> PackedValue = packedValue;
public ApplicationMetadata Application { get; init; } }
/// <summary> /// <summary>
/// The matched value from the Play Report. /// The matched value from the Play Report.
@@ -37,6 +39,17 @@ namespace Ryujinx.Ava.Utilities.PlayReport
: boxed.ToString(); : boxed.ToString();
} }
public static implicit operator Value(MessagePackObject matched) => new(matched);
public static Value[] ConvertPackedObjects(IEnumerable<MessagePackObject> packObjects)
=> packObjects.Select(packObject => new Value(packObject)).ToArray();
public static Dictionary<string, Value> ConvertPackedObjectMap(Dictionary<string, MessagePackObject> packObjects)
=> packObjects.ToDictionary(
x => x.Key,
x => new Value(x.Value)
);
#region AsX accessors #region AsX accessors
public bool BooleanValue => PackedValue.AsBoolean(); public bool BooleanValue => PackedValue.AsBoolean();