From 5845a44fc22140939dae4c218711e7d63a909cdc Mon Sep 17 00:00:00 2001 From: Thog Date: Mon, 8 Oct 2018 15:46:54 +0200 Subject: [PATCH] Implement IRoInterface This is required by Super Mario Party. This commit also adds MapProcessCodeMemory and UnmapProcessCodeMemory functions in KMemoryManager. Those two calls might not reflect what the SVC of the same names do. --- Ryujinx.HLE/HOS/Kernel/KMemoryManager.cs | 95 ++++ Ryujinx.HLE/HOS/Process.cs | 30 +- Ryujinx.HLE/HOS/Services/Ldr/IRoInterface.cs | 447 ++++++++++++++++++ Ryujinx.HLE/HOS/Services/ServiceFactory.cs | 4 + Ryujinx.HLE/Loaders/Executable.cs | 45 +- .../Loaders/Executables/IExecutable.cs | 3 + Ryujinx.HLE/Loaders/Executables/Nro.cs | 9 +- Ryujinx.HLE/Loaders/Executables/Nso.cs | 6 + Ryujinx.HLE/Logging/LogClass.cs | 1 + 9 files changed, 627 insertions(+), 13 deletions(-) create mode 100644 Ryujinx.HLE/HOS/Services/Ldr/IRoInterface.cs diff --git a/Ryujinx.HLE/HOS/Kernel/KMemoryManager.cs b/Ryujinx.HLE/HOS/Kernel/KMemoryManager.cs index 3afdb57022..2390a9229b 100644 --- a/Ryujinx.HLE/HOS/Kernel/KMemoryManager.cs +++ b/Ryujinx.HLE/HOS/Kernel/KMemoryManager.cs @@ -148,6 +148,90 @@ namespace Ryujinx.HLE.HOS.Kernel } } + public long MapProcessCodeMemory(long Dst, long Src, long Size) + { + lock (Blocks) + { + long PagesCount = Size / PageSize; + + bool Success = IsUnmapped(Dst, Size); + + Success &= CheckRange( + Src, + Size, + MemoryState.Mask, + MemoryState.Heap, + MemoryPermission.Mask, + MemoryPermission.ReadAndWrite, + MemoryAttribute.Mask, + MemoryAttribute.None, + MemoryAttribute.IpcAndDeviceMapped, + out _, + out _, + out _); + + if (Success) + { + long PA = CpuMemory.GetPhysicalAddress(Src); + + InsertBlock(Dst, PagesCount, MemoryState.CodeStatic, MemoryPermission.ReadAndExecute); + InsertBlock(Src, PagesCount, MemoryState.Heap, MemoryPermission.None); + + CpuMemory.Map(Dst, PA, Size); + return 0; + } + } + + return MakeError(ErrorModule.Kernel, KernelErr.NoAccessPerm); + } + + public long UnmapProcessCodeMemory(long Dst, long Src, long Size) + { + lock (Blocks) + { + long PagesCount = Size / PageSize; + + bool Success = CheckRange( + Dst, + Size, + MemoryState.Mask, + MemoryState.CodeStatic, + MemoryPermission.None, + MemoryPermission.None, + MemoryAttribute.Mask, + MemoryAttribute.None, + MemoryAttribute.IpcAndDeviceMapped, + out _, + out _, + out _); + + Success &= CheckRange( + Src, + Size, + MemoryState.Mask, + MemoryState.Heap, + MemoryPermission.Mask, + MemoryPermission.None, + MemoryAttribute.Mask, + MemoryAttribute.None, + MemoryAttribute.IpcAndDeviceMapped, + out _, + out _, + out _); + + if (Success) + { + InsertBlock(Dst, PagesCount, MemoryState.Unmapped); + InsertBlock(Src, PagesCount, MemoryState.Heap, MemoryPermission.ReadAndWrite); + + CpuMemory.Unmap(Dst, Size); + return 0; + } + } + + return MakeError(ErrorModule.Kernel, KernelErr.NoAccessPerm); + } + public void HleMapCustom(long Position, long Size, MemoryState State, MemoryPermission Permission) { long PagesCount = Size / PageSize; @@ -755,6 +839,17 @@ namespace Ryujinx.HLE.HOS.Kernel } } + public bool HleIsUnmapped(long Position, long Size) + { + bool Result = false; + + lock (Blocks) + { + Result = IsUnmapped(Position, Size); + } + return Result; + } + private bool IsUnmapped(long Position, long Size) { return CheckRange( diff --git a/Ryujinx.HLE/HOS/Process.cs b/Ryujinx.HLE/HOS/Process.cs index 3817f56195..ab0ab18ba1 100644 --- a/Ryujinx.HLE/HOS/Process.cs +++ b/Ryujinx.HLE/HOS/Process.cs @@ -106,13 +106,37 @@ namespace Ryujinx.HLE.HOS throw new ObjectDisposedException(nameof(Process)); } - Device.Log.PrintInfo(LogClass.Loader, $"Image base at 0x{ImageBase:x16}."); + long ImageEnd = LoadProgram(Program, ImageBase); - Executable Executable = new Executable(Program, MemoryManager, Memory, ImageBase); + ImageBase = IntUtils.AlignUp(ImageEnd, KMemoryManager.PageSize); + } + + public long LoadProgram(IExecutable Program, long ExecutableBase) + { + if (Disposed) + { + throw new ObjectDisposedException(nameof(Process)); + } + + Device.Log.PrintInfo(LogClass.Loader, $"Image base at 0x{ExecutableBase:x16}."); + + Executable Executable = new Executable(Program, MemoryManager, Memory, ExecutableBase); Executables.Add(Executable); - ImageBase = IntUtils.AlignUp(Executable.ImageEnd, KMemoryManager.PageSize); + return Executable.ImageEnd; + } + + public void RemoveProgram(long ExecutableBase) + { + foreach (Executable Executable in Executables) + { + if (Executable.ImageBase == ExecutableBase) + { + Executables.Remove(Executable); + break; + } + } } public void SetEmptyArgs() diff --git a/Ryujinx.HLE/HOS/Services/Ldr/IRoInterface.cs b/Ryujinx.HLE/HOS/Services/Ldr/IRoInterface.cs new file mode 100644 index 0000000000..3f8e28b95b --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Ldr/IRoInterface.cs @@ -0,0 +1,447 @@ +using Ryujinx.HLE.HOS.Ipc; +using Ryujinx.HLE.Loaders.Executables; +using Ryujinx.HLE.Utilities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security.Cryptography; + +namespace Ryujinx.HLE.HOS.Services.Ldr +{ + [StructLayout(LayoutKind.Explicit, Size = 0x350)] + unsafe struct NrrHeader + { + [FieldOffset(0)] + public uint Magic; + + [FieldOffset(0x10)] + public ulong TitleIdMask; + + [FieldOffset(0x18)] + public ulong TitleIdPattern; + + [FieldOffset(0x30)] + public fixed byte Modulus[0x100]; + + [FieldOffset(0x130)] + public fixed byte FixedKeySignature[0x100]; + + [FieldOffset(0x230)] + public fixed byte NrrSignature[0x100]; + + [FieldOffset(0x330)] + public ulong TitleIdMin; + + [FieldOffset(0x338)] + public uint NrrSize; + + [FieldOffset(0x340)] + public uint HashOffset; + + [FieldOffset(0x344)] + public uint HashCount; + } + + class NrrInfo + { + public NrrHeader Header { get; private set; } + public List Hashes { get; private set; } + public long NrrAddress { get; private set; } + + public NrrInfo(long NrrAddress, NrrHeader Header, List Hashes) + { + this.NrrAddress = NrrAddress; + this.Header = Header; + this.Hashes = Hashes; + } + } + + class NroInfo + { + public Nro Executable { get; private set; } + public byte[] Hash { get; private set; } + public long NroAddress { get; private set; } + public long TotalSize { get; private set; } + public long NroMappedAddress; + + + public NroInfo(Nro Executable, byte[] Hash, long TotalSize) + { + this.Executable = Executable; + this.Hash = Hash; + this.TotalSize = TotalSize; + } + } + + class IRoInterface : IpcService + { + private Dictionary m_Commands; + + public override IReadOnlyDictionary Commands => m_Commands; + + private const int MaxNrr = 0x40; + private const int MaxNro = 0x40; + + private List NrrInfos; + private List NroInfos; + + private bool IsInitialized; + + public IRoInterface() + { + m_Commands = new Dictionary() + { + { 0, LoadNro }, + { 1, UnloadNro }, + { 2, LoadNrr }, + { 3, UnloadNrr }, + { 4, Initialize }, + }; + + NrrInfos = new List(MaxNrr); + NroInfos = new List(MaxNro); + } + + private long ParseNrr(out NrrInfo NrrInfo, ServiceCtx Context, long NrrAddress, long NrrSize) + { + NrrInfo = null; + + if (NrrSize == 0 || NrrAddress + NrrSize <= NrrAddress || (NrrSize & 0xFFF) != 0) + { + return 0xA409; + } + else if ((NrrAddress & 0xFFF) != 0) + { + return 0xA209; + } + + StructReader Reader = new StructReader(Context.Memory, NrrAddress); + NrrHeader Header = Reader.Read(); + + if (Header.Magic != 0x3052524E) + { + return 0x6A09; + } + else if (Header.NrrSize != NrrSize) + { + return 0xA409; + } + + List Hashes = new List(); + + for (int i = 0; i < Header.HashCount; i++) + { + Hashes.Add(Context.Memory.ReadBytes(NrrAddress + Header.HashOffset + (i * 0x20), 0x20)); + } + + NrrInfo = new NrrInfo(NrrAddress, Header, Hashes); + + return 0; + } + + public bool IsNroHashPresent(byte[] NroHash) + { + foreach (NrrInfo Info in NrrInfos) + { + foreach (byte[] Hash in Info.Hashes) + { + if (Hash.SequenceEqual(NroHash)) + { + return true; + } + } + } + return false; + } + + public bool IsNroLoaded(byte[] NroHash) + { + foreach (NroInfo Info in NroInfos) + { + if (Info.Hash.SequenceEqual(NroHash)) + { + return true; + } + } + return false; + } + + public long ParseNro(out NroInfo Res, ServiceCtx Context, long NroHeapAddress, long NroSize, long BssHeapAddress, long BssSize) + { + Res = null; + + if (NroInfos.Count >= MaxNro) + { + return 0x6809; + } + else if (NroSize == 0 || NroHeapAddress + NroSize <= NroHeapAddress || (NroSize & 0xFFF) != 0) + { + return 0xA409; + } + else if (BssSize != 0 && (BssHeapAddress + BssSize) <= BssHeapAddress) + { + return 0xA409; + } + else if ((NroHeapAddress & 0xFFF) != 0) + { + return 0xA209; + } + + uint Magic = Context.Memory.ReadUInt32(NroHeapAddress + 0x10); + uint NroFileSize = Context.Memory.ReadUInt32(NroHeapAddress + 0x18); + + if (Magic != 0x304F524E || NroSize != NroFileSize) + { + return 0x6809; + } + + byte[] NroData = Context.Memory.ReadBytes(NroHeapAddress, NroSize); + byte[] NroHash = null; + + MemoryStream Stream = new MemoryStream(NroData); + + using (SHA256 Hasher = SHA256.Create()) + { + NroHash = Hasher.ComputeHash(Stream); + } + + if (!IsNroHashPresent(NroHash)) + { + return 0x6C09; + } + + if (IsNroLoaded(NroHash)) + { + return 0x7209; + } + + Stream.Position = 0; + + Nro Executable = new Nro(Stream, "memory", NroHeapAddress, BssHeapAddress); + + // check if everything is page align. + if ((Executable.Text.Length & 0xFFF) != 0 || (Executable.RO.Length & 0xFFF) != 0 + || (Executable.Data.Length & 0xFFF) != 0 || (Executable.BssSize & 0xFFF) != 0) + { + return 0x6C09; + } + + // check if everything is contiguous. + if (Executable.ROOffset != Executable.TextOffset + Executable.Text.Length + || Executable.DataOffset != Executable.ROOffset + Executable.RO.Length + || NroFileSize != Executable.DataOffset + Executable.Data.Length) + { + return 0x6C09; + } + + // finally check the bss size match. + if (Executable.BssSize != BssSize) + { + return 0x6C09; + } + + Res = new NroInfo(Executable, NroHash, Executable.Text.Length + Executable.RO.Length + Executable.Data.Length + Executable.BssSize); + return 0; + } + + private long MapNro(ServiceCtx Context, NroInfo Info, out long NroMappedAddress) + { + NroMappedAddress = 0; + long TargetAddress = Context.Process.MemoryManager.AddrSpaceStart; + + long HeapRegionStart = Context.Process.MemoryManager.HeapRegionStart; + long HeapRegionEnd = Context.Process.MemoryManager.HeapRegionEnd; + + long MapRegionStart = Context.Process.MemoryManager.MapRegionStart; + long MapRegionEnd = Context.Process.MemoryManager.MapRegionEnd; + + while (true) + { + if (TargetAddress + Info.TotalSize >= Context.Process.MemoryManager.AddrSpaceEnd) + { + return 0x6609; + } + + bool IsValidAddress = !(HeapRegionStart > 0 && HeapRegionStart <= TargetAddress + Info.TotalSize - 1 + && TargetAddress <= HeapRegionEnd - 1) + && !(MapRegionStart > 0 + && MapRegionStart <= TargetAddress + Info.TotalSize - 1 + && TargetAddress <= MapRegionEnd - 1); + + if (IsValidAddress && Context.Process.MemoryManager.HleIsUnmapped(TargetAddress, Info.TotalSize)) + { + break; + } + + TargetAddress += 0x1000; + } + + Context.Process.LoadProgram(Info.Executable, TargetAddress); + + Info.NroMappedAddress = TargetAddress; + NroMappedAddress = TargetAddress; + + return 0; + } + + private long RemoveNrrInfo(long NrrAddress) + { + foreach (NrrInfo Info in NrrInfos) + { + if (Info.NrrAddress == NrrAddress) + { + NrrInfos.Remove(Info); + + return 0; + } + } + + return 0xAA09; + } + + private long RemoveNroInfo(ServiceCtx Context, long NroMappedAddress, long NroHeapAddress) + { + foreach (NroInfo Info in NroInfos) + { + if (Info.NroMappedAddress == NroMappedAddress && Info.Executable.SourceAddress == NroHeapAddress) + { + NroInfos.Remove(Info); + + Context.Process.RemoveProgram(Info.NroMappedAddress); + + long Result = Context.Process.MemoryManager.UnmapProcessCodeMemory(Info.NroMappedAddress, Info.Executable.SourceAddress, Info.TotalSize - Info.Executable.BssSize); + + if (Result == 0 && Info.Executable.BssSize != 0) + { + Result = Context.Process.MemoryManager.UnmapProcessCodeMemory(Info.NroMappedAddress + Info.TotalSize - Info.Executable.BssSize, Info.Executable.BssAddress, Info.Executable.BssSize); + } + + return Result; + } + } + return 0xA809; + } + + // LoadNro(u64, u64, u64, u64, u64, pid) -> u64 + public long LoadNro(ServiceCtx Context) + { + long Result = 0xAE09; + + // Zero + Context.RequestData.ReadUInt64(); + + long NroHeapAddress = Context.RequestData.ReadInt64(); + long NroSize = Context.RequestData.ReadInt64(); + long BssHeapAddress = Context.RequestData.ReadInt64(); + long BssSize = Context.RequestData.ReadInt64(); + + long NroMappedAddress = 0; + + if (IsInitialized) + { + NroInfo Info; + + Result = ParseNro(out Info, Context, NroHeapAddress, NroSize, BssHeapAddress, BssSize); + + if (Result == 0) + { + Result = MapNro(Context, Info, out NroMappedAddress); + + if (Result == 0) + { + NroInfos.Add(Info); + } + } + } + + Context.ResponseData.Write(NroMappedAddress); + + return Result; + } + + // UnloadNro(u64, u64, pid) + public long UnloadNro(ServiceCtx Context) + { + long Result = 0xAE09; + + long NroMappedAddress = Context.RequestData.ReadInt64(); + long NroHeapAddress = Context.RequestData.ReadInt64(); + + if (IsInitialized) + { + if ((NroMappedAddress & 0xFFF) != 0 || (NroHeapAddress & 0xFFF) != 0) + { + return 0xA209; + } + + Result = RemoveNroInfo(Context, NroMappedAddress, NroHeapAddress); + } + + return Result; + } + + // LoadNrr(u64, u64, u64, pid) + public long LoadNrr(ServiceCtx Context) + { + long Result = 0xAE09; + + // Zero + Context.RequestData.ReadUInt64(); + + long NrrAddress = Context.RequestData.ReadInt64(); + long NrrSize = Context.RequestData.ReadInt64(); + + if (IsInitialized) + { + NrrInfo Info; + Result = ParseNrr(out Info, Context, NrrAddress, NrrSize); + + if(Result == 0) + { + if (NrrInfos.Count >= MaxNrr) + { + Result = 0x7009; + } + else + { + NrrInfos.Add(Info); + } + } + } + + return Result; + } + + // UnloadNrr(u64, u64, pid) + public long UnloadNrr(ServiceCtx Context) + { + long Result = 0xAE09; + + // Zero + Context.RequestData.ReadUInt64(); + + long NrrHeapAddress = Context.RequestData.ReadInt64(); + + if (IsInitialized) + { + if ((NrrHeapAddress & 0xFFF) != 0) + { + return 0xA209; + } + + Result = RemoveNrrInfo(NrrHeapAddress); + } + return Result; + } + + // Initialize(u64, pid, KObject) + public long Initialize(ServiceCtx Context) + { + // TODO: we actually ignore the pid and process handle receive, we will need to use them when we will have multi process support. + IsInitialized = true; + return 0; + } + } +} diff --git a/Ryujinx.HLE/HOS/Services/ServiceFactory.cs b/Ryujinx.HLE/HOS/Services/ServiceFactory.cs index fd5a06e6d9..f701dd0537 100644 --- a/Ryujinx.HLE/HOS/Services/ServiceFactory.cs +++ b/Ryujinx.HLE/HOS/Services/ServiceFactory.cs @@ -7,6 +7,7 @@ using Ryujinx.HLE.HOS.Services.Caps; using Ryujinx.HLE.HOS.Services.FspSrv; using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.HLE.HOS.Services.Irs; +using Ryujinx.HLE.HOS.Services.Ldr; using Ryujinx.HLE.HOS.Services.Lm; using Ryujinx.HLE.HOS.Services.Mm; using Ryujinx.HLE.HOS.Services.Nfp; @@ -100,6 +101,9 @@ namespace Ryujinx.HLE.HOS.Services case "irs": return new IIrSensorServer(); + case "ldr:ro": + return new IRoInterface(); + case "lm": return new ILogService(); diff --git a/Ryujinx.HLE/Loaders/Executable.cs b/Ryujinx.HLE/Loaders/Executable.cs index a9850e4af9..bb2614a972 100644 --- a/Ryujinx.HLE/Loaders/Executable.cs +++ b/Ryujinx.HLE/Loaders/Executable.cs @@ -49,21 +49,50 @@ namespace Ryujinx.HLE.Loaders long DataPosition = ImageBase + (uint)Exe.DataOffset; long TextSize = (uint)IntUtils.AlignUp(Exe.Text.Length, KMemoryManager.PageSize); - long ROSize = (uint)IntUtils.AlignUp(Exe.RO.Length, KMemoryManager.PageSize); + long ROSize = (uint)IntUtils.AlignUp(Exe.RO.Length, KMemoryManager.PageSize); long DataSize = (uint)IntUtils.AlignUp(Exe.Data.Length, KMemoryManager.PageSize); + long BssSize = (uint)IntUtils.AlignUp(Exe.BssSize, KMemoryManager.PageSize); - long DataAndBssSize = (uint)IntUtils.AlignUp(Exe.BssSize, KMemoryManager.PageSize) + DataSize; + long DataAndBssSize = BssSize + DataSize; ImageEnd = DataPosition + DataAndBssSize; - MemoryManager.HleMapProcessCode(TextPosition, TextSize + ROSize + DataAndBssSize); + if (Exe.SourceAddress == 0) + { + MemoryManager.HleMapProcessCode(TextPosition, TextSize + ROSize + DataAndBssSize); - MemoryManager.SetProcessMemoryPermission(ROPosition, ROSize, MemoryPermission.Read); - MemoryManager.SetProcessMemoryPermission(DataPosition, DataAndBssSize, MemoryPermission.ReadAndWrite); + MemoryManager.SetProcessMemoryPermission(ROPosition, ROSize, MemoryPermission.Read); + MemoryManager.SetProcessMemoryPermission(DataPosition, DataAndBssSize, MemoryPermission.ReadAndWrite); - Memory.WriteBytes(TextPosition, Exe.Text); - Memory.WriteBytes(ROPosition, Exe.RO); - Memory.WriteBytes(DataPosition, Exe.Data); + Memory.WriteBytes(TextPosition, Exe.Text); + Memory.WriteBytes(ROPosition, Exe.RO); + Memory.WriteBytes(DataPosition, Exe.Data); + + } + else + { + long Result = MemoryManager.MapProcessCodeMemory(TextPosition, Exe.SourceAddress, TextSize + ROSize + DataSize); + + if (Result != 0) + { + throw new InvalidOperationException(); + } + + MemoryManager.SetProcessMemoryPermission(ROPosition, ROSize, MemoryPermission.Read); + MemoryManager.SetProcessMemoryPermission(DataPosition, DataSize, MemoryPermission.ReadAndWrite); + + if (Exe.BssAddress != 0 && Exe.BssSize != 0) + { + Result = MemoryManager.MapProcessCodeMemory(DataPosition + DataSize, Exe.BssAddress, BssSize); + + if (Result != 0) + { + throw new InvalidOperationException(); + } + + MemoryManager.SetProcessMemoryPermission(DataPosition + DataSize, BssSize, MemoryPermission.ReadAndWrite); + } + } if (Exe.Mod0Offset == 0) { diff --git a/Ryujinx.HLE/Loaders/Executables/IExecutable.cs b/Ryujinx.HLE/Loaders/Executables/IExecutable.cs index 44bad61497..6f0952abdb 100644 --- a/Ryujinx.HLE/Loaders/Executables/IExecutable.cs +++ b/Ryujinx.HLE/Loaders/Executables/IExecutable.cs @@ -8,6 +8,9 @@ namespace Ryujinx.HLE.Loaders.Executables byte[] RO { get; } byte[] Data { get; } + long SourceAddress { get; } + long BssAddress { get; } + int Mod0Offset { get; } int TextOffset { get; } int ROOffset { get; } diff --git a/Ryujinx.HLE/Loaders/Executables/Nro.cs b/Ryujinx.HLE/Loaders/Executables/Nro.cs index 0b5068d7b9..6015da2132 100644 --- a/Ryujinx.HLE/Loaders/Executables/Nro.cs +++ b/Ryujinx.HLE/Loaders/Executables/Nro.cs @@ -16,9 +16,14 @@ namespace Ryujinx.HLE.Loaders.Executables public int DataOffset { get; private set; } public int BssSize { get; private set; } - public Nro(Stream Input, string FilePath) + public long SourceAddress { get; private set; } + public long BssAddress { get; private set; } + + public Nro(Stream Input, string FilePath, long SourceAddress = 0, long BssAddress = 0) { - this.FilePath = FilePath; + this.FilePath = FilePath; + this.SourceAddress = SourceAddress; + this.BssAddress = BssAddress; BinaryReader Reader = new BinaryReader(Input); diff --git a/Ryujinx.HLE/Loaders/Executables/Nso.cs b/Ryujinx.HLE/Loaders/Executables/Nso.cs index fef9c4b853..7f18466bb3 100644 --- a/Ryujinx.HLE/Loaders/Executables/Nso.cs +++ b/Ryujinx.HLE/Loaders/Executables/Nso.cs @@ -18,6 +18,9 @@ namespace Ryujinx.HLE.Loaders.Executables public int DataOffset { get; private set; } public int BssSize { get; private set; } + public long SourceAddress { get; private set; } + public long BssAddress { get; private set; } + [Flags] private enum NsoFlags { @@ -33,6 +36,9 @@ namespace Ryujinx.HLE.Loaders.Executables { this.FilePath = FilePath; + SourceAddress = 0; + BssAddress = 0; + BinaryReader Reader = new BinaryReader(Input); Input.Seek(0, SeekOrigin.Begin); diff --git a/Ryujinx.HLE/Logging/LogClass.cs b/Ryujinx.HLE/Logging/LogClass.cs index 4905013411..0458c75f8a 100644 --- a/Ryujinx.HLE/Logging/LogClass.cs +++ b/Ryujinx.HLE/Logging/LogClass.cs @@ -23,6 +23,7 @@ namespace Ryujinx.HLE.Logging ServiceFs, ServiceHid, ServiceIrs, + ServiceLdr, ServiceLm, ServiceMm, ServiceNfp,