Implement IRoInterface (#445)

* 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.

* Fix some code style issues

* Use MakeError to clarify error code

* Add NRR and NRO constants

* Fix some codestyle issues

* Fix InvalidMemoryState error code
This commit is contained in:
Thomas Guillemard 2018-10-10 01:01:49 +02:00 committed by Ac_K
parent 65c67bb4a5
commit d5c0de8362
10 changed files with 657 additions and 13 deletions

View file

@ -148,6 +148,92 @@ 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) public void HleMapCustom(long Position, long Size, MemoryState State, MemoryPermission Permission)
{ {
long PagesCount = Size / PageSize; long PagesCount = Size / PageSize;
@ -755,6 +841,18 @@ 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) private bool IsUnmapped(long Position, long Size)
{ {
return CheckRange( return CheckRange(

View file

@ -106,13 +106,37 @@ namespace Ryujinx.HLE.HOS
throw new ObjectDisposedException(nameof(Process)); 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); 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() public void SetEmptyArgs()

View file

@ -0,0 +1,457 @@
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;
using static Ryujinx.HLE.HOS.ErrorCode;
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<byte[]> Hashes { get; private set; }
public long NrrAddress { get; private set; }
public NrrInfo(long NrrAddress, NrrHeader Header, List<byte[]> 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 { get; set; }
public NroInfo(Nro Executable, byte[] Hash, long TotalSize)
{
this.Executable = Executable;
this.Hash = Hash;
this.TotalSize = TotalSize;
}
}
class IRoInterface : IpcService
{
private Dictionary<int, ServiceProcessRequest> m_Commands;
public override IReadOnlyDictionary<int, ServiceProcessRequest> Commands => m_Commands;
private const int MaxNrr = 0x40;
private const int MaxNro = 0x40;
private const uint NrrMagic = 0x3052524E;
private const uint NroMagic = 0x304F524E;
private List<NrrInfo> NrrInfos;
private List<NroInfo> NroInfos;
private bool IsInitialized;
public IRoInterface()
{
m_Commands = new Dictionary<int, ServiceProcessRequest>()
{
{ 0, LoadNro },
{ 1, UnloadNro },
{ 2, LoadNrr },
{ 3, UnloadNrr },
{ 4, Initialize },
};
NrrInfos = new List<NrrInfo>(MaxNrr);
NroInfos = new List<NroInfo>(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 MakeError(ErrorModule.Loader, LoaderErr.BadSize);
}
else if ((NrrAddress & 0xFFF) != 0)
{
return MakeError(ErrorModule.Loader, LoaderErr.UnalignedAddress);
}
StructReader Reader = new StructReader(Context.Memory, NrrAddress);
NrrHeader Header = Reader.Read<NrrHeader>();
if (Header.Magic != NrrMagic)
{
return MakeError(ErrorModule.Loader, LoaderErr.InvalidNrr);
}
else if (Header.NrrSize != NrrSize)
{
return MakeError(ErrorModule.Loader, LoaderErr.BadSize);
}
List<byte[]> Hashes = new List<byte[]>();
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 MakeError(ErrorModule.Loader, LoaderErr.MaxNro);
}
else if (NroSize == 0 || NroHeapAddress + NroSize <= NroHeapAddress || (NroSize & 0xFFF) != 0)
{
return MakeError(ErrorModule.Loader, LoaderErr.BadSize);
}
else if (BssSize != 0 && (BssHeapAddress + BssSize) <= BssHeapAddress)
{
return MakeError(ErrorModule.Loader, LoaderErr.BadSize);
}
else if ((NroHeapAddress & 0xFFF) != 0)
{
return MakeError(ErrorModule.Loader, LoaderErr.UnalignedAddress);
}
uint Magic = Context.Memory.ReadUInt32(NroHeapAddress + 0x10);
uint NroFileSize = Context.Memory.ReadUInt32(NroHeapAddress + 0x18);
if (Magic != NroMagic || NroSize != NroFileSize)
{
return MakeError(ErrorModule.Loader, LoaderErr.InvalidNro);
}
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 MakeError(ErrorModule.Loader, LoaderErr.NroHashNotPresent);
}
if (IsNroLoaded(NroHash))
{
return MakeError(ErrorModule.Loader, LoaderErr.NroAlreadyLoaded);
}
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 MakeError(ErrorModule.Loader, LoaderErr.InvalidNro);
}
// 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 MakeError(ErrorModule.Loader, LoaderErr.InvalidNro);
}
// finally check the bss size match.
if (Executable.BssSize != BssSize)
{
return MakeError(ErrorModule.Loader, LoaderErr.InvalidNro);
}
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 MakeError(ErrorModule.Loader, LoaderErr.InvalidMemoryState);
}
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 MakeError(ErrorModule.Loader, LoaderErr.BadNrrAddress);
}
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 MakeError(ErrorModule.Loader, LoaderErr.BadNroAddress);
}
// LoadNro(u64, u64, u64, u64, u64, pid) -> u64
public long LoadNro(ServiceCtx Context)
{
long Result = MakeError(ErrorModule.Loader, LoaderErr.BadInitialization);
// 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 = MakeError(ErrorModule.Loader, LoaderErr.BadInitialization);
long NroMappedAddress = Context.RequestData.ReadInt64();
long NroHeapAddress = Context.RequestData.ReadInt64();
if (IsInitialized)
{
if ((NroMappedAddress & 0xFFF) != 0 || (NroHeapAddress & 0xFFF) != 0)
{
return MakeError(ErrorModule.Loader, LoaderErr.UnalignedAddress);
}
Result = RemoveNroInfo(Context, NroMappedAddress, NroHeapAddress);
}
return Result;
}
// LoadNrr(u64, u64, u64, pid)
public long LoadNrr(ServiceCtx Context)
{
long Result = MakeError(ErrorModule.Loader, LoaderErr.BadInitialization);
// 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 = MakeError(ErrorModule.Loader, LoaderErr.MaxNrr);
}
else
{
NrrInfos.Add(Info);
}
}
}
return Result;
}
// UnloadNrr(u64, u64, pid)
public long UnloadNrr(ServiceCtx Context)
{
long Result = MakeError(ErrorModule.Loader, LoaderErr.BadInitialization);
// Zero
Context.RequestData.ReadUInt64();
long NrrHeapAddress = Context.RequestData.ReadInt64();
if (IsInitialized)
{
if ((NrrHeapAddress & 0xFFF) != 0)
{
return MakeError(ErrorModule.Loader, LoaderErr.UnalignedAddress);
}
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;
}
}
}

View file

@ -0,0 +1,18 @@
namespace Ryujinx.HLE.HOS.Services.Ldr
{
static class LoaderErr
{
public const int InvalidMemoryState = 51;
public const int InvalidNro = 52;
public const int InvalidNrr = 53;
public const int MaxNro = 55;
public const int MaxNrr = 56;
public const int NroAlreadyLoaded = 57;
public const int NroHashNotPresent = 54;
public const int UnalignedAddress = 81;
public const int BadSize = 82;
public const int BadNroAddress = 84;
public const int BadNrrAddress = 85;
public const int BadInitialization = 87;
}
}

View file

@ -7,6 +7,7 @@ using Ryujinx.HLE.HOS.Services.Caps;
using Ryujinx.HLE.HOS.Services.FspSrv; using Ryujinx.HLE.HOS.Services.FspSrv;
using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.HLE.HOS.Services.Irs; using Ryujinx.HLE.HOS.Services.Irs;
using Ryujinx.HLE.HOS.Services.Ldr;
using Ryujinx.HLE.HOS.Services.Lm; using Ryujinx.HLE.HOS.Services.Lm;
using Ryujinx.HLE.HOS.Services.Mm; using Ryujinx.HLE.HOS.Services.Mm;
using Ryujinx.HLE.HOS.Services.Nfp; using Ryujinx.HLE.HOS.Services.Nfp;
@ -100,6 +101,9 @@ namespace Ryujinx.HLE.HOS.Services
case "irs": case "irs":
return new IIrSensorServer(); return new IIrSensorServer();
case "ldr:ro":
return new IRoInterface();
case "lm": case "lm":
return new ILogService(); return new ILogService();

View file

@ -49,21 +49,49 @@ namespace Ryujinx.HLE.Loaders
long DataPosition = ImageBase + (uint)Exe.DataOffset; long DataPosition = ImageBase + (uint)Exe.DataOffset;
long TextSize = (uint)IntUtils.AlignUp(Exe.Text.Length, KMemoryManager.PageSize); 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 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; 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(ROPosition, ROSize, MemoryPermission.Read);
MemoryManager.SetProcessMemoryPermission(DataPosition, DataAndBssSize, MemoryPermission.ReadAndWrite); MemoryManager.SetProcessMemoryPermission(DataPosition, DataAndBssSize, MemoryPermission.ReadAndWrite);
Memory.WriteBytes(TextPosition, Exe.Text); Memory.WriteBytes(TextPosition, Exe.Text);
Memory.WriteBytes(ROPosition, Exe.RO); Memory.WriteBytes(ROPosition, Exe.RO);
Memory.WriteBytes(DataPosition, Exe.Data); 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) if (Exe.Mod0Offset == 0)
{ {

View file

@ -8,6 +8,9 @@ namespace Ryujinx.HLE.Loaders.Executables
byte[] RO { get; } byte[] RO { get; }
byte[] Data { get; } byte[] Data { get; }
long SourceAddress { get; }
long BssAddress { get; }
int Mod0Offset { get; } int Mod0Offset { get; }
int TextOffset { get; } int TextOffset { get; }
int ROOffset { get; } int ROOffset { get; }

View file

@ -16,9 +16,14 @@ namespace Ryujinx.HLE.Loaders.Executables
public int DataOffset { get; private set; } public int DataOffset { get; private set; }
public int BssSize { 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); BinaryReader Reader = new BinaryReader(Input);

View file

@ -18,6 +18,9 @@ namespace Ryujinx.HLE.Loaders.Executables
public int DataOffset { get; private set; } public int DataOffset { get; private set; }
public int BssSize { get; private set; } public int BssSize { get; private set; }
public long SourceAddress { get; private set; }
public long BssAddress { get; private set; }
[Flags] [Flags]
private enum NsoFlags private enum NsoFlags
{ {
@ -33,6 +36,9 @@ namespace Ryujinx.HLE.Loaders.Executables
{ {
this.FilePath = FilePath; this.FilePath = FilePath;
SourceAddress = 0;
BssAddress = 0;
BinaryReader Reader = new BinaryReader(Input); BinaryReader Reader = new BinaryReader(Input);
Input.Seek(0, SeekOrigin.Begin); Input.Seek(0, SeekOrigin.Begin);

View file

@ -23,6 +23,7 @@ namespace Ryujinx.HLE.Logging
ServiceFs, ServiceFs,
ServiceHid, ServiceHid,
ServiceIrs, ServiceIrs,
ServiceLdr,
ServiceLm, ServiceLm,
ServiceMm, ServiceMm,
ServiceNfp, ServiceNfp,