From 08b315b5fcdffeb8387a1a2f799cfbc02e3c1ee6 Mon Sep 17 00:00:00 2001 From: ParantezTech Date: Sun, 21 Jun 2026 23:18:18 +0300 Subject: [PATCH] [kernel] Add guest memory support --- src/SharpEmu.Core/Cpu/TrackedCpuMemory.cs | 13 +- .../Memory/PhysicalVirtualMemory.cs | 64 ++- src/SharpEmu.HLE/IGuestMemoryAllocator.cs | 9 + .../Kernel/KernelMemoryCompatExports.cs | 387 ++++++++++++++++-- .../Kernel/KernelRuntimeCompatExports.cs | 192 ++++++++- 5 files changed, 612 insertions(+), 53 deletions(-) create mode 100644 src/SharpEmu.HLE/IGuestMemoryAllocator.cs diff --git a/src/SharpEmu.Core/Cpu/TrackedCpuMemory.cs b/src/SharpEmu.Core/Cpu/TrackedCpuMemory.cs index 3561508..239dc61 100644 --- a/src/SharpEmu.Core/Cpu/TrackedCpuMemory.cs +++ b/src/SharpEmu.Core/Cpu/TrackedCpuMemory.cs @@ -5,7 +5,7 @@ using SharpEmu.HLE; namespace SharpEmu.Core.Cpu; -public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory +public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator { private readonly ICpuMemory _inner; @@ -39,4 +39,15 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory return result; } + + public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address) + { + if (_inner is IGuestMemoryAllocator allocator) + { + return allocator.TryAllocateGuestMemory(size, alignment, out address); + } + + address = 0; + return false; + } } diff --git a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs index 802515f..f34006f 100644 --- a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs +++ b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs @@ -7,13 +7,17 @@ using SharpEmu.HLE; namespace SharpEmu.Core.Memory; -public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable +public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable { private readonly object _gate = new(); + private readonly object _guestAllocationGate = new(); private readonly List _regions = new(); private readonly Dictionary _pageProtections = new(); private bool _disposed; private const ulong PageSize = 0x1000; + private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000; + private const ulong GuestAllocationArenaSize = 0x0100_0000; + private const ulong GuestAllocationArenaStartOffset = PageSize; private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB private const ulong LazyReservePrimeBytes = 0x5000_0000UL; // 1.25 GiB private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB @@ -29,6 +33,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable private const uint PAGE_READWRITE = 0x04; private const uint PAGE_READONLY = 0x02; + private ulong _guestAllocationArenaBase; + private ulong _guestAllocationOffset; + [DllImport("kernel32.dll", SetLastError = true)] private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect); @@ -215,16 +222,61 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable return actualAddress; } + public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address) + { + address = 0; + if (size == 0 || alignment == 0 || (alignment & (alignment - 1)) != 0) + { + return false; + } + + lock (_guestAllocationGate) + { + if (_guestAllocationArenaBase == 0) + { + try + { + _guestAllocationArenaBase = AllocateAt( + GuestAllocationArenaAddress, + GuestAllocationArenaSize, + executable: false, + allowAlternative: true); + _guestAllocationOffset = GuestAllocationArenaStartOffset; + } + catch (Exception) + { + return false; + } + } + + var alignedOffset = AlignUp(_guestAllocationOffset, alignment); + if (alignedOffset > GuestAllocationArenaSize || size > GuestAllocationArenaSize - alignedOffset) + { + return false; + } + + address = _guestAllocationArenaBase + alignedOffset; + _guestAllocationOffset = alignedOffset + size; + return true; + } + } + public void Clear() { - lock (_gate) + lock (_guestAllocationGate) { - foreach (var region in _regions) + lock (_gate) { - VirtualFree((void*)region.VirtualAddress, 0, MEM_RELEASE); + foreach (var region in _regions) + { + VirtualFree((void*)region.VirtualAddress, 0, MEM_RELEASE); + } + _regions.Clear(); + _pageProtections.Clear(); } - _regions.Clear(); - _pageProtections.Clear(); + + _guestAllocationArenaBase = 0; + _guestAllocationOffset = 0; } } diff --git a/src/SharpEmu.HLE/IGuestMemoryAllocator.cs b/src/SharpEmu.HLE/IGuestMemoryAllocator.cs new file mode 100644 index 0000000..c67c778 --- /dev/null +++ b/src/SharpEmu.HLE/IGuestMemoryAllocator.cs @@ -0,0 +1,9 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace SharpEmu.HLE; + +public interface IGuestMemoryAllocator +{ + bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address); +} diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 92db412..275f081 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -38,6 +38,13 @@ public static class KernelMemoryCompatExports private const int OrbisKernelBatchMapEntryProtectionOffset = 24; private const int OrbisKernelBatchMapEntryTypeOffset = 25; private const int OrbisKernelBatchMapEntryOperationOffset = 28; + private const ulong OrbisPageSize = 0x4000; + private const int OrbisProtCpuRead = 0x01; + private const int OrbisProtCpuWrite = 0x02; + private const int OrbisProtCpuExec = 0x04; + private const int OrbisProtGpuRead = 0x10; + private const int OrbisProtGpuWrite = 0x20; + private const int OrbisProtCpuReadWrite = OrbisProtCpuRead | OrbisProtCpuWrite; private const int SeekSet = 0; private const int SeekCur = 1; private const int SeekEnd = 2; @@ -47,14 +54,15 @@ public static class KernelMemoryCompatExports private const int OrbisVirtualQueryInfoSize = 72; private const int OrbisKernelMaximumNameLength = 32; private const uint MemCommit = 0x1000; - private const uint PageNoAccess = 0x01; - private const uint PageReadOnly = 0x02; - private const uint PageReadWrite = 0x04; - private const uint PageWriteCopy = 0x08; - private const uint PageExecuteRead = 0x20; - private const uint PageExecuteReadWrite = 0x40; - private const uint PageExecuteWriteCopy = 0x80; - private const uint PageGuard = 0x100; + private const uint HostPageNoAccess = 0x01; + private const uint HostPageReadOnly = 0x02; + private const uint HostPageReadWrite = 0x04; + private const uint HostPageWriteCopy = 0x08; + private const uint HostPageExecute = 0x10; + private const uint HostPageExecuteRead = 0x20; + private const uint HostPageExecuteReadWrite = 0x40; + private const uint HostPageExecuteWriteCopy = 0x80; + private const uint HostPageGuard = 0x100; private const int Enomem = 12; private const int Einval = 22; private const int Erange = 34; @@ -121,6 +129,10 @@ public static class KernelMemoryCompatExports [DllImport("kernel32.dll", SetLastError = true)] private static extern nuint VirtualQuery(nint lpAddress, out MemoryBasicInformation lpBuffer, nuint dwLength); + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool VirtualProtect(nint lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect); + private sealed class OpenDirectory { public required string Path { get; init; } @@ -130,9 +142,59 @@ public static class KernelMemoryCompatExports private readonly record struct DirectAllocation(ulong Start, ulong Length, int MemoryType); private readonly record struct LibcHeapAllocation(nint BaseAddress, nuint Size, nuint Alignment); - private readonly record struct MappedRegion(ulong Address, ulong Length, int Protection, bool IsFlexible, ulong DirectStart); + private readonly record struct MappedRegion(ulong Address, ulong Length, int Protection, bool IsFlexible, bool IsDirect, ulong DirectStart); private readonly record struct BatchMapEntry(ulong Start, ulong Offset, ulong Length, byte Protection, byte Type, int Operation); + internal static bool TryAllocateHleData( + CpuContext ctx, + ulong length, + ulong alignment, + out ulong address) + { + address = 0; + if (length == 0 || length > int.MaxValue) + { + return false; + } + + var mappedLength = AlignUp(length, 0x1000UL); + var effectiveAlignment = Math.Max(alignment, 0x1000UL); + lock (_memoryGate) + { + var desiredAddress = AlignUp( + _nextVirtualAddress == 0 ? 0x1_0000_0000UL : _nextVirtualAddress, + effectiveAlignment); + if (!TryReserveGuestVirtualRange(ctx, desiredAddress, mappedLength, OrbisProtCpuReadWrite, out address) || + address == 0) + { + return false; + } + + _nextVirtualAddress = Math.Max(_nextVirtualAddress, address + mappedLength); + _mappedRegions[address] = new MappedRegion( + address, + mappedLength, + OrbisProtCpuReadWrite, + IsFlexible: false, + IsDirect: false, + DirectStart: 0); + } + + var zeroes = new byte[(int)Math.Min(mappedLength, (ulong)MemsetChunkSize)]; + for (ulong offset = 0; offset < mappedLength;) + { + var chunkLength = (int)Math.Min((ulong)zeroes.Length, mappedLength - offset); + if (!ctx.Memory.TryWrite(address + offset, zeroes.AsSpan(0, chunkLength))) + { + return false; + } + + offset += (ulong)chunkLength; + } + + return true; + } + [SysAbiExport( Nid = "8zTFvBIAIN8", ExportName = "memset", @@ -1238,6 +1300,13 @@ public static class KernelMemoryCompatExports LibraryName = "libKernel")] public static int KernelCloseUnderscore(CpuContext ctx) => KernelCloseCore(ctx, unchecked((int)ctx[CpuRegister.Rdi])); + [SysAbiExport( + Nid = "bY-PO6JhzhQ", + ExportName = "close", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixClose(CpuContext ctx) => KernelCloseCore(ctx, unchecked((int)ctx[CpuRegister.Rdi])); + [SysAbiExport( Nid = "UK2Tl2DWUns", ExportName = "sceKernelClose", @@ -1444,6 +1513,20 @@ public static class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "AqBioC2vF3I", + ExportName = "read", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixRead(CpuContext ctx) => KernelReadUnderscore(ctx); + + [SysAbiExport( + Nid = "Cg4srZ6TKbU", + ExportName = "sceKernelRead", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelRead(CpuContext ctx) => KernelReadUnderscore(ctx); + [SysAbiExport( Nid = "Oy6IpwgtYOk", ExportName = "lseek", @@ -1629,6 +1712,20 @@ public static class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "FN4gaPmuFV8", + ExportName = "write", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixWrite(CpuContext ctx) => KernelWriteUnderscore(ctx); + + [SysAbiExport( + Nid = "4wSze92BhLI", + ExportName = "sceKernelWrite", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelWrite(CpuContext ctx) => KernelWriteUnderscore(ctx); + [SysAbiExport( Nid = "lLMT9vJAck0", ExportName = "clock_gettime", @@ -2172,7 +2269,13 @@ public static class KernelMemoryCompatExports } _nextVirtualAddress = Math.Max(_nextVirtualAddress, mappedAddress + length); - _mappedRegions[mappedAddress] = new MappedRegion(mappedAddress, length, protection, IsFlexible: false, DirectStart: directMemoryStart); + _mappedRegions[mappedAddress] = new MappedRegion( + mappedAddress, + length, + protection, + IsFlexible: false, + IsDirect: true, + DirectStart: directMemoryStart); } if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress)) @@ -2245,7 +2348,13 @@ public static class KernelMemoryCompatExports _nextVirtualAddress = Math.Max(_nextVirtualAddress, mappedAddress + length); _allocatedFlexibleBytes = Math.Min(FlexibleMemorySizeBytes, _allocatedFlexibleBytes + length); - _mappedRegions[mappedAddress] = new MappedRegion(mappedAddress, length, protection, IsFlexible: true, DirectStart: 0); + _mappedRegions[mappedAddress] = new MappedRegion( + mappedAddress, + length, + protection, + IsFlexible: true, + IsDirect: false, + DirectStart: 0); } if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress)) @@ -2344,8 +2453,7 @@ public static class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } - if (region.DirectStart != 0 && - _directAllocations.TryGetValue(region.DirectStart, out var allocation)) + if (region.IsDirect && TryFindDirectAllocationLocked(region.DirectStart, out var allocation)) { memoryType = allocation.MemoryType; } @@ -2363,7 +2471,7 @@ public static class KernelMemoryCompatExports stateFlags |= 0x01u; } - if (region.DirectStart != 0) + if (region.IsDirect) { stateFlags |= 0x02u; } @@ -2377,7 +2485,7 @@ public static class KernelMemoryCompatExports BinaryPrimitives.WriteInt32LittleEndian(payload[28..32], memoryType); payload[32] = unchecked((byte)stateFlags); - var name = region.DirectStart != 0 + var name = region.IsDirect ? "direct" : region.IsFlexible ? "flexible" @@ -2495,12 +2603,19 @@ public static class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } + if (!TryNormalizeProtectRange(address, length, out var alignedAddress, out var alignedLength)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + if (!TryProtectHostRange(alignedAddress, alignedLength, protection)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + lock (_memoryGate) { - if (!TryApplyMappedRegionProtectionLocked(address, length, protection)) - { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; - } + _ = TryApplyMappedRegionProtectionLocked(alignedAddress, alignedLength, protection); } return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -2522,12 +2637,19 @@ public static class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } + if (!TryNormalizeProtectRange(address, length, out var alignedAddress, out var alignedLength)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + if (!TryProtectHostRange(alignedAddress, alignedLength, protection)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + lock (_memoryGate) { - if (!TryApplyMappedRegionProtectionLocked(address, length, protection, memoryType)) - { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; - } + _ = TryApplyMappedRegionProtectionLocked(alignedAddress, alignedLength, protection, memoryType); } return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -3562,7 +3684,7 @@ public static class KernelMemoryCompatExports return false; } - var executable = (protection & 0x04) != 0; + var executable = (protection & OrbisProtCpuExec) != 0; var invokeArgs = allocateAtHasAllowAlternativeArg ? new object[] { desiredAddress, length, executable, true } : new object[] { desiredAddress, length, executable }; @@ -3692,6 +3814,25 @@ public static class KernelMemoryCompatExports return temp0Root; } + var hostappRoot = ResolveHostappRoot(); + if (guestPath.StartsWith("/hostapp/", StringComparison.OrdinalIgnoreCase)) + { + var relative = NormalizeMountRelativePath(guestPath["/hostapp/".Length..]); + return Path.Combine(hostappRoot, relative); + } + + if (guestPath.StartsWith("hostapp/", StringComparison.OrdinalIgnoreCase)) + { + var relative = NormalizeMountRelativePath(guestPath["hostapp/".Length..]); + return Path.Combine(hostappRoot, relative); + } + + if (string.Equals(guestPath, "/hostapp", StringComparison.OrdinalIgnoreCase) || + string.Equals(guestPath, "hostapp", StringComparison.OrdinalIgnoreCase)) + { + return hostappRoot; + } + var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR"); if (!string.IsNullOrWhiteSpace(app0Root)) { @@ -3775,6 +3916,25 @@ public static class KernelMemoryCompatExports return root; } + private static string ResolveHostappRoot() + { + const string hostappVariableName = "SHARPEMU_HOSTAPP_DIR"; + var configuredRoot = Environment.GetEnvironmentVariable(hostappVariableName); + string root; + if (!string.IsNullOrWhiteSpace(configuredRoot)) + { + root = Path.GetFullPath(configuredRoot); + } + else + { + root = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "logs", "hostapp")); + Environment.SetEnvironmentVariable(hostappVariableName, root); + } + + Directory.CreateDirectory(root); + return root; + } + private static void EnsureOpenParentDirectoryExists(string guestPath, string hostPath, int flags) { if (string.IsNullOrWhiteSpace(hostPath)) @@ -4336,23 +4496,171 @@ public static class KernelMemoryCompatExports int protection, int? memoryType = null) { - if (!_mappedRegions.TryGetValue(address, out var region) || region.Length != length) + if (length == 0 || !TryAddU64(address, length, out var endAddress)) { return false; } - _mappedRegions[address] = region with { Protection = protection }; - - if (memoryType.HasValue && - region.DirectStart != 0 && - _directAllocations.TryGetValue(region.DirectStart, out var allocation)) + var affected = new List(); + var cursor = address; + foreach (var region in _mappedRegions.Values.OrderBy(static region => region.Address)) { - _directAllocations[region.DirectStart] = allocation with { MemoryType = memoryType.Value }; + if (!TryAddU64(region.Address, region.Length, out var regionEnd) || regionEnd <= cursor) + { + continue; + } + + if (region.Address > cursor) + { + return false; + } + + affected.Add(region); + cursor = regionEnd >= endAddress ? endAddress : regionEnd; + if (cursor == endAddress) + { + break; + } + } + + if (cursor != endAddress) + { + return false; + } + + foreach (var region in affected) + { + _mappedRegions.Remove(region.Address); + } + + foreach (var region in affected) + { + var regionEnd = region.Address + region.Length; + var protectStart = Math.Max(region.Address, address); + var protectEnd = Math.Min(regionEnd, endAddress); + + if (region.Address < protectStart) + { + AddMappedRegionSliceLocked(region, region.Address, protectStart, region.Protection); + } + + AddMappedRegionSliceLocked(region, protectStart, protectEnd, protection); + + if (protectEnd < regionEnd) + { + AddMappedRegionSliceLocked(region, protectEnd, regionEnd, region.Protection); + } + + if (memoryType.HasValue && region.IsDirect && TryFindDirectAllocationLocked(region.DirectStart, out var allocation)) + { + _directAllocations[allocation.Start] = allocation with { MemoryType = memoryType.Value }; + } } return true; } + private static void AddMappedRegionSliceLocked( + MappedRegion source, + ulong start, + ulong end, + int protection) + { + if (end <= start) + { + return; + } + + var directStart = source.IsDirect + ? unchecked(source.DirectStart + (start - source.Address)) + : 0UL; + + _mappedRegions[start] = source with + { + Address = start, + Length = end - start, + Protection = protection, + DirectStart = directStart, + }; + } + + private static bool TryFindDirectAllocationLocked(ulong directStart, out DirectAllocation allocation) + { + foreach (var candidate in _directAllocations.Values) + { + if (!TryAddU64(candidate.Start, candidate.Length, out var candidateEnd)) + { + continue; + } + + if (directStart >= candidate.Start && directStart < candidateEnd) + { + allocation = candidate; + return true; + } + } + + allocation = default; + return false; + } + + private static bool TryNormalizeProtectRange( + ulong address, + ulong length, + out ulong alignedAddress, + out ulong alignedLength) + { + alignedAddress = 0; + alignedLength = 0; + if (length == 0 || !TryAddU64(address, length, out var endAddress)) + { + return false; + } + + alignedAddress = AlignDown(address, OrbisPageSize); + var alignedEnd = AlignUp(endAddress, OrbisPageSize); + alignedLength = alignedEnd - alignedAddress; + return alignedLength != 0; + } + + private static bool TryProtectHostRange(ulong address, ulong length, int orbisProtection) + { + if (length == 0 || length > nuint.MaxValue) + { + return false; + } + + var hostProtection = ResolveHostProtection(orbisProtection); + if (!VirtualProtect((nint)address, (nuint)length, hostProtection, out _)) + { + return false; + } + + return true; + } + + private static uint ResolveHostProtection(int orbisProtection) + { + var read = (orbisProtection & (OrbisProtCpuRead | OrbisProtGpuRead)) != 0; + var write = (orbisProtection & (OrbisProtCpuWrite | OrbisProtGpuWrite)) != 0; + var execute = (orbisProtection & OrbisProtCpuExec) != 0; + + if (execute) + { + return write + ? HostPageExecuteReadWrite + : read + ? HostPageExecuteRead + : HostPageExecute; + } + + return write + ? HostPageReadWrite + : read + ? HostPageReadOnly + : HostPageNoAccess; + } + private static bool TryFindVirtualQueryRegionLocked(ulong queryAddress, bool findNext, out MappedRegion region) { region = default; @@ -4907,13 +5215,13 @@ public static class KernelMemoryCompatExports private static bool HasRequiredProtection(uint protect, bool writeAccess) { - if ((protect & (PageNoAccess | PageGuard)) != 0) + if ((protect & (HostPageNoAccess | HostPageGuard)) != 0) { return false; } - const uint readableMask = PageReadOnly | PageReadWrite | PageWriteCopy | PageExecuteRead | PageExecuteReadWrite | PageExecuteWriteCopy; - const uint writableMask = PageReadWrite | PageWriteCopy | PageExecuteReadWrite | PageExecuteWriteCopy; + const uint readableMask = HostPageReadOnly | HostPageReadWrite | HostPageWriteCopy | HostPageExecuteRead | HostPageExecuteReadWrite | HostPageExecuteWriteCopy; + const uint writableMask = HostPageReadWrite | HostPageWriteCopy | HostPageExecuteReadWrite | HostPageExecuteWriteCopy; var expected = writeAccess ? writableMask : readableMask; return (protect & expected) != 0; } @@ -5233,6 +5541,17 @@ public static class KernelMemoryCompatExports return (value + mask) & ~mask; } + private static ulong AlignDown(ulong value, ulong alignment) + { + if (alignment <= 1) + { + return value; + } + + var mask = alignment - 1; + return value & ~mask; + } + private static bool TryAddU64(ulong left, ulong right, out ulong sum) { sum = left + right; diff --git a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs index ae5d387..c7cac09 100644 --- a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs @@ -25,6 +25,7 @@ public static class KernelRuntimeCompatExports private const ulong TlsNewReplaceOffset = 0x300; private const int MallocReplaceSize = 0x70; private const int NewReplaceSize = 0x68; + private const int OrbisTimesecSize = sizeof(long) + sizeof(uint) + sizeof(uint); private const ulong ModuleInfoHandleOffset = 0x108; private const ulong ModuleInfoNameOffset = 0x10; private const int ModuleInfoNameMaxBytes = 64; @@ -52,6 +53,7 @@ public static class KernelRuntimeCompatExports private static readonly object _prtApertureGate = new(); private static readonly (ulong Base, ulong Size)[] _prtApertures = new (ulong Base, ulong Size)[3]; private static int _stackChkFailCount; + private static long _usleepTraceCount; [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate ulong RdtscDelegate(); @@ -64,33 +66,52 @@ public static class KernelRuntimeCompatExports public static int KernelUsleep(CpuContext ctx) { var micros = ctx[CpuRegister.Rdi]; + TraceUsleepSpin(ctx, micros); if (micros == 0) { ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - var sleepMilliseconds = (int)Math.Min(micros / 1000UL, int.MaxValue); - if (sleepMilliseconds > 0) + if (micros < 1000) { - Thread.Sleep(sleepMilliseconds); + Thread.Yield(); } - - var remainingMicros = micros % 1000UL; - if (remainingMicros > 0) + else { - var targetTicks = (long)((double)remainingMicros * Stopwatch.Frequency / 1_000_000.0); - var spin = Stopwatch.StartNew(); - while (spin.ElapsedTicks < targetTicks) - { - Thread.SpinWait(16); - } + var sleepMilliseconds = (int)Math.Min((micros + 999UL) / 1000UL, int.MaxValue); + Thread.Sleep(sleepMilliseconds); } ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + private static void TraceUsleepSpin(CpuContext ctx, ulong micros) + { + if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal)) + { + return; + } + + var count = Interlocked.Increment(ref _usleepTraceCount); + if (count > 32 && count % 10000 != 0) + { + return; + } + + var rbx = ctx[CpuRegister.Rbx]; + var lockAddress = rbx == 0 ? 0 : rbx + 0xF78; + var lockText = "unreadable"; + if (lockAddress != 0 && ctx.TryReadUInt64(lockAddress, out var lockValue)) + { + lockText = $"0x{lockValue:X16}"; + } + + Console.Error.WriteLine( + $"[LOADER][TRACE] usleep#{count}: usec={micros} rbx=0x{rbx:X16} lock@+F78=0x{lockAddress:X16}:{lockText} r13=0x{ctx[CpuRegister.R13]:X16} r14=0x{ctx[CpuRegister.R14]:X16} r15=0x{ctx[CpuRegister.R15]:X16}"); + } + [SysAbiExport( Nid = "QBi7HCK03hw", ExportName = "sceKernelClockGettime", @@ -156,6 +177,37 @@ public static class KernelRuntimeCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "n88vx3C5nW8", + ExportName = "gettimeofday", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixGettimeofday(CpuContext ctx) + { + var timeAddress = ctx[CpuRegister.Rdi]; + var timezoneAddress = ctx[CpuRegister.Rsi]; + var now = DateTimeOffset.UtcNow; + var seconds = now.ToUnixTimeSeconds(); + var microseconds = (now.Ticks % TimeSpan.TicksPerSecond) / 10; + + if (timeAddress != 0 && + (!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) || + !ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)microseconds)))) + { + return -1; + } + + if (timezoneAddress != 0 && + (!TryWriteInt32(ctx, timezoneAddress, 0) || + !TryWriteInt32(ctx, timezoneAddress + sizeof(int), 0))) + { + return -1; + } + + ctx[CpuRegister.Rax] = 0; + return 0; + } + [SysAbiExport( Nid = "-2IRUCO--PM", ExportName = "sceKernelReadTsc", @@ -552,6 +604,17 @@ public static class KernelRuntimeCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "jh+8XiK4LeE", + ExportName = "sceKernelIsAddressSanitizerEnabled", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelIsAddressSanitizerEnabled(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "ca7v6Cxulzs", ExportName = "sceKernelSetGPO", @@ -1024,6 +1087,111 @@ public static class KernelRuntimeCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "-o5uEDpN+oY", + ExportName = "sceKernelConvertUtcToLocaltime", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelConvertUtcToLocaltime(CpuContext ctx) + { + var utcSeconds = unchecked((long)ctx[CpuRegister.Rdi]); + var localTimeAddress = ctx[CpuRegister.Rsi]; + var timesecAddress = ctx[CpuRegister.Rdx]; + var dstSecondsAddress = ctx[CpuRegister.Rcx]; + + if (localTimeAddress == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + var utc = DateTimeOffset.FromUnixTimeSeconds(utcSeconds); + var local = TimeZoneInfo.ConvertTime(utc, TimeZoneInfo.Local); + var offset = local.Offset; + var localSeconds = utcSeconds + (long)offset.TotalSeconds; + var dstSeconds = TimeZoneInfo.Local.IsDaylightSavingTime(local.DateTime) + ? (uint)Math.Max(0, TimeZoneInfo.Local.GetAdjustmentRules() + .Where(rule => rule.DateStart <= local.Date && rule.DateEnd >= local.Date) + .Select(rule => rule.DaylightDelta.TotalSeconds) + .DefaultIfEmpty(0) + .Max()) + : 0u; + var westSeconds = unchecked((uint)(int)offset.TotalSeconds); + + if (!ctx.TryWriteUInt64(localTimeAddress, unchecked((ulong)localSeconds))) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (timesecAddress != 0) + { + Span timesec = stackalloc byte[OrbisTimesecSize]; + BinaryPrimitives.WriteInt64LittleEndian(timesec, utcSeconds); + BinaryPrimitives.WriteUInt32LittleEndian(timesec.Slice(sizeof(long), sizeof(uint)), westSeconds); + BinaryPrimitives.WriteUInt32LittleEndian(timesec.Slice(sizeof(long) + sizeof(uint), sizeof(uint)), dstSeconds); + if (!ctx.Memory.TryWrite(timesecAddress, timesec)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + } + + if (dstSecondsAddress != 0 && !ctx.TryWriteUInt64(dstSecondsAddress, dstSeconds)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "0NTHN1NKONI", + ExportName = "sceKernelConvertLocaltimeToUtc", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelConvertLocaltimeToUtc(CpuContext ctx) + { + var localSeconds = unchecked((long)ctx[CpuRegister.Rdi]); + var utcTimeAddress = ctx[CpuRegister.Rdx]; + var timezoneAddress = ctx[CpuRegister.Rcx]; + var dstSecondsAddress = ctx[CpuRegister.R8]; + + if (timezoneAddress == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + var localDate = DateTimeOffset.FromUnixTimeSeconds(localSeconds).DateTime; + var offset = TimeZoneInfo.Local.GetUtcOffset(localDate); + var utcSeconds = localSeconds - (long)offset.TotalSeconds; + var dstSeconds = TimeZoneInfo.Local.IsDaylightSavingTime(localDate) + ? (int)Math.Max(0, TimeZoneInfo.Local.GetAdjustmentRules() + .Where(rule => rule.DateStart <= localDate.Date && rule.DateEnd >= localDate.Date) + .Select(rule => rule.DaylightDelta.TotalSeconds) + .DefaultIfEmpty(0) + .Max()) + : 0; + var minutesWest = unchecked((int)-offset.TotalMinutes); + + if (!TryWriteInt32(ctx, timezoneAddress, minutesWest) || + !TryWriteInt32(ctx, timezoneAddress + sizeof(int), dstSeconds / 60)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (utcTimeAddress != 0 && !ctx.TryWriteUInt64(utcTimeAddress, unchecked((ulong)utcSeconds))) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (dstSecondsAddress != 0 && !TryWriteInt32(ctx, dstSecondsAddress, dstSeconds)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "vYU8P9Td2Zo", ExportName = "sceKernelAioInitializeImpl",