Archived
1
0
Fork 0
forked from Mirror/Ryujinx

PPTC meets ExeFS Patching. (#1865)

* PPTC meets ExeFS Patching.

* InternalVersion = 1865

* Ready!

* Optimized the PtcProfiler Load/Save methods.
This commit is contained in:
LDj3SNuD 2021-05-13 20:05:15 +02:00 committed by GitHub
parent a8022ca3a1
commit 57ea3f93a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 190 additions and 166 deletions

View file

@ -12,6 +12,8 @@ namespace ARMeilleure.Memory
T ReadTracked<T>(ulong va) where T : unmanaged;
void Write<T>(ulong va, T value) where T : unmanaged;
ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false);
ref T GetRef<T>(ulong va) where T : unmanaged;
bool IsMapped(ulong va);

View file

@ -28,7 +28,7 @@ namespace ARMeilleure.Translation.PTC
private const string OuterHeaderMagicString = "PTCohd\0\0";
private const string InnerHeaderMagicString = "PTCihd\0\0";
private const uint InternalVersion = 2190; //! To be incremented manually for each change to the ARMeilleure project.
private const uint InternalVersion = 1866; //! To be incremented manually for each change to the ARMeilleure project.
private const string ActualDir = "0";
private const string BackupDir = "1";
@ -50,8 +50,6 @@ namespace ARMeilleure.Translation.PTC
private static MemoryStream _relocsStream;
private static MemoryStream _unwindInfosStream;
private static BinaryWriter _infosWriter;
private static readonly ulong _outerHeaderMagic;
private static readonly ulong _innerHeaderMagic;
@ -153,14 +151,10 @@ namespace ARMeilleure.Translation.PTC
_codesList = new List<byte[]>();
_relocsStream = new MemoryStream();
_unwindInfosStream = new MemoryStream();
_infosWriter = new BinaryWriter(_infosStream, EncodingCache.UTF8NoBOM, true);
}
private static void DisposeCarriers()
{
_infosWriter.Dispose();
_infosStream.Dispose();
_codesList.Clear();
_relocsStream.Dispose();
@ -551,46 +545,33 @@ namespace ARMeilleure.Translation.PTC
return;
}
long infosStreamLength = _infosStream.Length;
long relocsStreamLength = _relocsStream.Length;
long unwindInfosStreamLength = _unwindInfosStream.Length;
_infosStream.Seek(0L, SeekOrigin.Begin);
_relocsStream.Seek(0L, SeekOrigin.Begin);
_unwindInfosStream.Seek(0L, SeekOrigin.Begin);
using (BinaryReader infosReader = new(_infosStream, EncodingCache.UTF8NoBOM, true))
using (BinaryReader relocsReader = new(_relocsStream, EncodingCache.UTF8NoBOM, true))
using (BinaryReader unwindInfosReader = new(_unwindInfosStream, EncodingCache.UTF8NoBOM, true))
{
for (int index = 0; index < GetEntriesCount(); index++)
{
InfoEntry infoEntry = ReadInfo(infosReader);
InfoEntry infoEntry = DeserializeStructure<InfoEntry>(_infosStream);
if (infoEntry.Stubbed)
{
SkipCode(index, infoEntry.CodeLength);
SkipReloc(infoEntry.RelocEntriesCount);
SkipUnwindInfo(unwindInfosReader);
continue;
}
else if (infoEntry.HighCq || !PtcProfiler.ProfiledFuncs.TryGetValue(infoEntry.Address, out var value) || !value.HighCq)
{
byte[] code = ReadCode(index, infoEntry.CodeLength);
Counter<uint> callCounter = null;
bool isEntryChanged = infoEntry.Hash != ComputeHash(memory, infoEntry.Address, infoEntry.GuestSize);
if (infoEntry.RelocEntriesCount != 0)
{
RelocEntry[] relocEntries = GetRelocEntries(relocsReader, infoEntry.RelocEntriesCount);
PatchCode(code, relocEntries, memory.PageTablePointer, jumpTable, countTable, out callCounter);
}
UnwindInfo unwindInfo = ReadUnwindInfo(unwindInfosReader);
TranslatedFunction func = FastTranslate(code, callCounter, infoEntry.GuestSize, unwindInfo, infoEntry.HighCq);
bool isAddressUnique = funcs.TryAdd(infoEntry.Address, func);
Debug.Assert(isAddressUnique, $"The address 0x{infoEntry.Address:X16} is not unique.");
}
else
if (isEntryChanged || (!infoEntry.HighCq && PtcProfiler.ProfiledFuncs.TryGetValue(infoEntry.Address, out var value) && value.HighCq))
{
infoEntry.Stubbed = true;
infoEntry.CodeLength = 0;
@ -599,15 +580,43 @@ namespace ARMeilleure.Translation.PTC
StubCode(index);
StubReloc(infoEntry.RelocEntriesCount);
StubUnwindInfo(unwindInfosReader);
if (isEntryChanged)
{
PtcJumpTable.Clean(infoEntry.Address);
Logger.Info?.Print(LogClass.Ptc, $"Invalidated translated function (address: 0x{infoEntry.Address:X16})");
}
continue;
}
byte[] code = ReadCode(index, infoEntry.CodeLength);
Counter<uint> callCounter = null;
if (infoEntry.RelocEntriesCount != 0)
{
RelocEntry[] relocEntries = GetRelocEntries(relocsReader, infoEntry.RelocEntriesCount);
PatchCode(code, relocEntries, memory.PageTablePointer, jumpTable, countTable, out callCounter);
}
UnwindInfo unwindInfo = ReadUnwindInfo(unwindInfosReader);
TranslatedFunction func = FastTranslate(code, callCounter, infoEntry.GuestSize, unwindInfo, infoEntry.HighCq);
bool isAddressUnique = funcs.TryAdd(infoEntry.Address, func);
Debug.Assert(isAddressUnique, $"The address 0x{infoEntry.Address:X16} is not unique.");
}
}
if (_infosStream.Position < _infosStream.Length ||
_relocsStream.Position < _relocsStream.Length ||
_unwindInfosStream.Position < _unwindInfosStream.Length)
if (_infosStream.Length != infosStreamLength || _infosStream.Position != infosStreamLength ||
_relocsStream.Length != relocsStreamLength || _relocsStream.Position != relocsStreamLength ||
_unwindInfosStream.Length != unwindInfosStreamLength || _unwindInfosStream.Position != unwindInfosStreamLength)
{
throw new Exception("Could not reach the end of one or more memory streams.");
throw new Exception("The length of a memory stream has changed, or its position has not reached or has exceeded its end.");
}
jumpTable.Initialize(PtcJumpTable, funcs);
@ -623,20 +632,6 @@ namespace ARMeilleure.Translation.PTC
return _codesList.Count;
}
private static InfoEntry ReadInfo(BinaryReader infosReader)
{
InfoEntry infoEntry = new InfoEntry();
infoEntry.Address = infosReader.ReadUInt64();
infoEntry.GuestSize = infosReader.ReadUInt64();
infoEntry.HighCq = infosReader.ReadBoolean();
infoEntry.Stubbed = infosReader.ReadBoolean();
infoEntry.CodeLength = infosReader.ReadInt32();
infoEntry.RelocEntriesCount = infosReader.ReadInt32();
return infoEntry;
}
[Conditional("DEBUG")]
private static void SkipCode(int index, int codeLength)
{
@ -764,15 +759,9 @@ namespace ARMeilleure.Translation.PTC
private static void UpdateInfo(InfoEntry infoEntry)
{
_infosStream.Seek(-InfoEntry.Stride, SeekOrigin.Current);
_infosStream.Seek(-Unsafe.SizeOf<InfoEntry>(), SeekOrigin.Current);
// WriteInfo.
_infosWriter.Write((ulong)infoEntry.Address);
_infosWriter.Write((ulong)infoEntry.GuestSize);
_infosWriter.Write((bool)infoEntry.HighCq);
_infosWriter.Write((bool)infoEntry.Stubbed);
_infosWriter.Write((int)infoEntry.CodeLength);
_infosWriter.Write((int)infoEntry.RelocEntriesCount);
SerializeStructure(_infosStream, infoEntry);
}
private static void StubCode(int index)
@ -844,7 +833,7 @@ namespace ARMeilleure.Translation.PTC
Debug.Assert(PtcProfiler.IsAddressInStaticCodeRange(address));
TranslatedFunction func = Translator.Translate(memory, jumpTable, countTable, address, item.mode, item.highCq);
TranslatedFunction func = Translator.Translate(memory, jumpTable, countTable, address, item.funcProfile.Mode, item.funcProfile.HighCq);
bool isAddressUnique = funcs.TryAdd(address, func);
@ -919,17 +908,26 @@ namespace ARMeilleure.Translation.PTC
while (!endEvent.WaitOne(refreshRate));
}
internal static void WriteInfoCodeRelocUnwindInfo(ulong address, ulong guestSize, bool highCq, PtcInfo ptcInfo)
internal static Hash128 ComputeHash(IMemoryManager memory, ulong address, ulong guestSize)
{
return XXHash128.ComputeHash(memory.GetSpan(address, checked((int)(guestSize))));
}
internal static void WriteInfoCodeRelocUnwindInfo(ulong address, ulong guestSize, Hash128 hash, bool highCq, PtcInfo ptcInfo)
{
lock (_lock)
{
// WriteInfo.
_infosWriter.Write((ulong)address); // InfoEntry.Address
_infosWriter.Write((ulong)guestSize); // InfoEntry.GuestSize
_infosWriter.Write((bool)highCq); // InfoEntry.HighCq
_infosWriter.Write((bool)false); // InfoEntry.Stubbed
_infosWriter.Write((int)ptcInfo.Code.Length); // InfoEntry.CodeLength
_infosWriter.Write((int)ptcInfo.RelocEntriesCount); // InfoEntry.RelocEntriesCount
InfoEntry infoEntry = new InfoEntry();
infoEntry.Address = address;
infoEntry.GuestSize = guestSize;
infoEntry.Hash = hash;
infoEntry.HighCq = highCq;
infoEntry.Stubbed = false;
infoEntry.CodeLength = ptcInfo.Code.Length;
infoEntry.RelocEntriesCount = ptcInfo.RelocEntriesCount;
SerializeStructure(_infosStream, infoEntry);
WriteCode(ptcInfo.Code.AsSpan());
@ -946,7 +944,7 @@ namespace ARMeilleure.Translation.PTC
_codesList.Add(code.ToArray());
}
private static bool GetEndianness()
internal static bool GetEndianness()
{
return BitConverter.IsLittleEndian;
}
@ -1032,12 +1030,12 @@ namespace ARMeilleure.Translation.PTC
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 42*/)]
private struct InfoEntry
{
public const int Stride = 26; // Bytes.
public ulong Address;
public ulong GuestSize;
public Hash128 Hash;
public bool HighCq;
public bool Stubbed;
public int CodeLength;

View file

@ -50,7 +50,12 @@ namespace ARMeilleure.Translation.PTC
T structure = default(T);
Span<T> spanT = MemoryMarshal.CreateSpan(ref structure, 1);
stream.Read(MemoryMarshal.AsBytes(spanT));
int bytesCount = stream.Read(MemoryMarshal.AsBytes(spanT));
if (bytesCount != Unsafe.SizeOf<T>())
{
throw new EndOfStreamException();
}
return structure;
}
@ -130,7 +135,12 @@ namespace ARMeilleure.Translation.PTC
T[] item = new T[itemLength];
stream.Read(MemoryMarshal.AsBytes(item.AsSpan()));
int bytesCount = stream.Read(MemoryMarshal.AsBytes(item.AsSpan()));
if (bytesCount != itemLength)
{
throw new EndOfStreamException();
}
list.Add(item);
}

View file

@ -129,7 +129,6 @@ namespace ARMeilleure.Translation.PTC
}
}
// For future use.
public void Clean(ulong guestAddress)
{
if (Owners.TryGetValue(guestAddress, out List<int> entries))

View file

@ -1,13 +1,15 @@
using ARMeilleure.State;
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using System;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Threading;
using static ARMeilleure.Translation.PTC.PtcFormatter;
@ -16,9 +18,9 @@ namespace ARMeilleure.Translation.PTC
{
public static class PtcProfiler
{
private const string HeaderMagic = "Phd";
private const string OuterHeaderMagicString = "Pohd\0\0\0\0";
private const uint InternalVersion = 1713; //! Not to be incremented manually for each change to the ARMeilleure project.
private const uint InternalVersion = 1866; //! Not to be incremented manually for each change to the ARMeilleure project.
private const int SaveInterval = 30; // Seconds.
@ -26,13 +28,15 @@ namespace ARMeilleure.Translation.PTC
private static readonly System.Timers.Timer _timer;
private static readonly ulong _outerHeaderMagic;
private static readonly ManualResetEvent _waitEvent;
private static readonly object _lock;
private static bool _disposed;
private static byte[] _lastHash;
private static Hash128 _lastHash;
internal static Dictionary<ulong, FuncProfile> ProfiledFuncs { get; private set; }
@ -46,6 +50,8 @@ namespace ARMeilleure.Translation.PTC
_timer = new System.Timers.Timer((double)SaveInterval * 1000d);
_timer.Elapsed += PreSave;
_outerHeaderMagic = BinaryPrimitives.ReadUInt64LittleEndian(EncodingCache.UTF8NoBOM.GetBytes(OuterHeaderMagicString).AsSpan());
_waitEvent = new ManualResetEvent(true);
_lock = new object();
@ -90,17 +96,15 @@ namespace ARMeilleure.Translation.PTC
return address >= StaticCodeStart && address < StaticCodeStart + StaticCodeSize;
}
internal static ConcurrentQueue<(ulong address, ExecutionMode mode, bool highCq)> GetProfiledFuncsToTranslate(ConcurrentDictionary<ulong, TranslatedFunction> funcs)
internal static ConcurrentQueue<(ulong address, FuncProfile funcProfile)> GetProfiledFuncsToTranslate(ConcurrentDictionary<ulong, TranslatedFunction> funcs)
{
var profiledFuncsToTranslate = new ConcurrentQueue<(ulong address, ExecutionMode mode, bool highCq)>();
var profiledFuncsToTranslate = new ConcurrentQueue<(ulong address, FuncProfile funcProfile)>();
foreach (var profiledFunc in ProfiledFuncs)
{
ulong address = profiledFunc.Key;
if (!funcs.ContainsKey(address))
if (!funcs.ContainsKey(profiledFunc.Key))
{
profiledFuncsToTranslate.Enqueue((address, profiledFunc.Value.Mode, profiledFunc.Value.HighCq));
profiledFuncsToTranslate.Enqueue((profiledFunc.Key, profiledFunc.Value));
}
}
@ -115,7 +119,7 @@ namespace ARMeilleure.Translation.PTC
internal static void PreLoad()
{
_lastHash = Array.Empty<byte>();
_lastHash = default;
string fileNameActual = string.Concat(Ptc.CachePathActual, ".info");
string fileNameBackup = string.Concat(Ptc.CachePathBackup, ".info");
@ -141,70 +145,75 @@ namespace ARMeilleure.Translation.PTC
private static bool Load(string fileName, bool isBackup)
{
using (FileStream compressedStream = new FileStream(fileName, FileMode.Open))
using (DeflateStream deflateStream = new DeflateStream(compressedStream, CompressionMode.Decompress, true))
using (MemoryStream stream = new MemoryStream())
using (MD5 md5 = MD5.Create())
using (FileStream compressedStream = new(fileName, FileMode.Open))
using (DeflateStream deflateStream = new(compressedStream, CompressionMode.Decompress, true))
{
try
{
deflateStream.CopyTo(stream);
}
catch
OuterHeader outerHeader = DeserializeStructure<OuterHeader>(compressedStream);
if (!outerHeader.IsHeaderValid())
{
InvalidateCompressedStream(compressedStream);
return false;
}
int hashSize = md5.HashSize / 8;
stream.Seek(0L, SeekOrigin.Begin);
byte[] currentHash = new byte[hashSize];
stream.Read(currentHash, 0, hashSize);
byte[] expectedHash = md5.ComputeHash(stream);
if (!CompareHash(currentHash, expectedHash))
if (outerHeader.Magic != _outerHeaderMagic)
{
InvalidateCompressedStream(compressedStream);
return false;
}
stream.Seek((long)hashSize, SeekOrigin.Begin);
Header header = ReadHeader(stream);
if (header.Magic != HeaderMagic)
if (outerHeader.InfoFileVersion != InternalVersion)
{
InvalidateCompressedStream(compressedStream);
return false;
}
if (header.InfoFileVersion != InternalVersion)
if (outerHeader.Endianness != Ptc.GetEndianness())
{
InvalidateCompressedStream(compressedStream);
return false;
}
try
using (MemoryStream stream = new MemoryStream())
{
Debug.Assert(stream.Seek(0L, SeekOrigin.Begin) == 0L && stream.Length == 0L);
try
{
deflateStream.CopyTo(stream);
}
catch
{
InvalidateCompressedStream(compressedStream);
return false;
}
Debug.Assert(stream.Position == stream.Length);
stream.Seek(0L, SeekOrigin.Begin);
Hash128 expectedHash = DeserializeStructure<Hash128>(stream);
Hash128 actualHash = XXHash128.ComputeHash(GetReadOnlySpan(stream));
if (actualHash != expectedHash)
{
InvalidateCompressedStream(compressedStream);
return false;
}
ProfiledFuncs = Deserialize(stream);
Debug.Assert(stream.Position == stream.Length);
_lastHash = actualHash;
}
catch
{
ProfiledFuncs = new Dictionary<ulong, FuncProfile>();
InvalidateCompressedStream(compressedStream);
return false;
}
_lastHash = expectedHash;
}
long fileSize = new FileInfo(fileName).Length;
@ -214,30 +223,16 @@ namespace ARMeilleure.Translation.PTC
return true;
}
private static bool CompareHash(ReadOnlySpan<byte> currentHash, ReadOnlySpan<byte> expectedHash)
{
return currentHash.SequenceEqual(expectedHash);
}
private static Header ReadHeader(Stream stream)
{
using (BinaryReader headerReader = new BinaryReader(stream, EncodingCache.UTF8NoBOM, true))
{
Header header = new Header();
header.Magic = headerReader.ReadString();
header.InfoFileVersion = headerReader.ReadUInt32();
return header;
}
}
private static Dictionary<ulong, FuncProfile> Deserialize(Stream stream)
{
return DeserializeDictionary<ulong, FuncProfile>(stream, (stream) => DeserializeStructure<FuncProfile>(stream));
}
private static ReadOnlySpan<byte> GetReadOnlySpan(MemoryStream memoryStream)
{
return new(memoryStream.GetBuffer(), (int)memoryStream.Position, (int)memoryStream.Length - (int)memoryStream.Position);
}
private static void InvalidateCompressedStream(FileStream compressedStream)
{
compressedStream.SetLength(0L);
@ -266,14 +261,20 @@ namespace ARMeilleure.Translation.PTC
{
int profiledFuncsCount;
OuterHeader outerHeader = new OuterHeader();
outerHeader.Magic = _outerHeaderMagic;
outerHeader.InfoFileVersion = InternalVersion;
outerHeader.Endianness = Ptc.GetEndianness();
outerHeader.SetHeaderHash();
using (MemoryStream stream = new MemoryStream())
using (MD5 md5 = MD5.Create())
{
int hashSize = md5.HashSize / 8;
Debug.Assert(stream.Seek(0L, SeekOrigin.Begin) == 0L && stream.Length == 0L);
stream.Seek((long)hashSize, SeekOrigin.Begin);
WriteHeader(stream);
stream.Seek((long)Unsafe.SizeOf<Hash128>(), SeekOrigin.Begin);
lock (_lock)
{
@ -282,22 +283,26 @@ namespace ARMeilleure.Translation.PTC
profiledFuncsCount = ProfiledFuncs.Count;
}
stream.Seek((long)hashSize, SeekOrigin.Begin);
byte[] hash = md5.ComputeHash(stream);
Debug.Assert(stream.Position == stream.Length);
stream.Seek((long)Unsafe.SizeOf<Hash128>(), SeekOrigin.Begin);
Hash128 hash = XXHash128.ComputeHash(GetReadOnlySpan(stream));
stream.Seek(0L, SeekOrigin.Begin);
stream.Write(hash, 0, hashSize);
SerializeStructure(stream, hash);
if (CompareHash(hash, _lastHash))
if (hash == _lastHash)
{
return;
}
using (FileStream compressedStream = new FileStream(fileName, FileMode.OpenOrCreate))
using (DeflateStream deflateStream = new DeflateStream(compressedStream, SaveCompressionLevel, true))
using (FileStream compressedStream = new(fileName, FileMode.OpenOrCreate))
using (DeflateStream deflateStream = new(compressedStream, SaveCompressionLevel, true))
{
try
{
SerializeStructure(compressedStream, outerHeader);
stream.WriteTo(deflateStream);
_lastHash = hash;
@ -306,7 +311,7 @@ namespace ARMeilleure.Translation.PTC
{
compressedStream.Position = 0L;
_lastHash = Array.Empty<byte>();
_lastHash = default;
}
if (compressedStream.Position < compressedStream.Length)
@ -324,26 +329,35 @@ namespace ARMeilleure.Translation.PTC
}
}
private static void WriteHeader(Stream stream)
{
using (BinaryWriter headerWriter = new BinaryWriter(stream, EncodingCache.UTF8NoBOM, true))
{
headerWriter.Write((string)HeaderMagic); // Header.Magic
headerWriter.Write((uint)InternalVersion); // Header.InfoFileVersion
}
}
private static void Serialize(Stream stream, Dictionary<ulong, FuncProfile> profiledFuncs)
{
SerializeDictionary(stream, profiledFuncs, (stream, structure) => SerializeStructure(stream, structure));
}
private struct Header
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 29*/)]
private struct OuterHeader
{
public string Magic;
public ulong Magic;
public uint InfoFileVersion;
public bool Endianness;
public Hash128 HeaderHash;
public void SetHeaderHash()
{
Span<OuterHeader> spanHeader = MemoryMarshal.CreateSpan(ref this, 1);
HeaderHash = XXHash128.ComputeHash(MemoryMarshal.AsBytes(spanHeader).Slice(0, Unsafe.SizeOf<OuterHeader>() - Unsafe.SizeOf<Hash128>()));
}
public bool IsHeaderValid()
{
Span<OuterHeader> spanHeader = MemoryMarshal.CreateSpan(ref this, 1);
return XXHash128.ComputeHash(MemoryMarshal.AsBytes(spanHeader).Slice(0, Unsafe.SizeOf<OuterHeader>() - Unsafe.SizeOf<Hash128>())) == HeaderHash;
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 5*/)]

View file

@ -7,6 +7,7 @@ using ARMeilleure.Memory;
using ARMeilleure.State;
using ARMeilleure.Translation.Cache;
using ARMeilleure.Translation.PTC;
using Ryujinx.Common;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@ -282,7 +283,9 @@ namespace ARMeilleure.Translation
ResetPool(highCq ? 1 : 0);
Ptc.WriteInfoCodeRelocUnwindInfo(address, funcSize, highCq, ptcInfo);
Hash128 hash = Ptc.ComputeHash(memory, address, funcSize);
Ptc.WriteInfoCodeRelocUnwindInfo(address, funcSize, hash, highCq, ptcInfo);
}
return new TranslatedFunction(func, counter, funcSize, highCq);

View file

@ -507,20 +507,18 @@ namespace Ryujinx.HLE.HOS
metaData = modLoadResult.Npdm;
}
bool hasPatches = _fileSystem.ModLoader.ApplyNsoPatches(TitleId, programs);
_fileSystem.ModLoader.ApplyNsoPatches(TitleId, programs);
_contentManager.LoadEntries(_device);
bool usePtc = _device.System.EnablePtc;
// don't use PTC if exefs files have been replaced
// Don't use PPTC if ExeFs files have been replaced.
usePtc &= !modLoadResult.Modified;
// don't use PTC if exefs files have been patched
usePtc &= !hasPatches;
if (_device.System.EnablePtc && !usePtc)
{
Logger.Warning?.Print(LogClass.Ptc, $"Detected exefs modifications. PPTC disabled.");
Logger.Warning?.Print(LogClass.Ptc, $"Detected unsupported ExeFs modifications. PPTC disabled.");
}
Graphics.Gpu.GraphicsConfig.TitleId = TitleIdText;