From bbf5ff7be82c3e7afed9653b203cc3fd1ec4efce Mon Sep 17 00:00:00 2001 From: ParantezTech Date: Sun, 10 May 2026 19:51:57 +0300 Subject: [PATCH] more HLE's and fix cpu execution some titles --- src/SharpEmu.Core/Cpu/CpuDispatcher.cs | 22 + .../Native/DirectExecutionBackend.Imports.cs | 60 ++ .../Cpu/Native/DirectExecutionBackend.cs | 647 +++++++++++++++++- src/SharpEmu.HLE/GuestThreadExecution.cs | 107 +++ src/SharpEmu.Libs/Ampr/AmprExports.cs | 168 ++++- .../Kernel/KernelEventQueueCompatExports.cs | 7 + src/SharpEmu.Libs/Kernel/KernelExports.cs | 31 +- .../Kernel/KernelMemoryCompatExports.cs | 107 ++- .../Kernel/KernelPthreadCompatExports.cs | 59 +- .../KernelPthreadExtendedCompatExports.cs | 119 +++- .../Kernel/KernelPthreadState.cs | 13 + 11 files changed, 1275 insertions(+), 65 deletions(-) create mode 100644 src/SharpEmu.HLE/GuestThreadExecution.cs diff --git a/src/SharpEmu.Core/Cpu/CpuDispatcher.cs b/src/SharpEmu.Core/Cpu/CpuDispatcher.cs index 64d3d96..2a39fab 100644 --- a/src/SharpEmu.Core/Cpu/CpuDispatcher.cs +++ b/src/SharpEmu.Core/Cpu/CpuDispatcher.cs @@ -193,6 +193,11 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } + if (!InitializeGuestFrameChainSentinel(context)) + { + return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + if (!InitializeTls(context, tlsBase)) { return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); @@ -363,6 +368,23 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBABEUL); } + private static bool InitializeGuestFrameChainSentinel(CpuContext context) + { + var stackTop = context[CpuRegister.Rsp] + sizeof(ulong); + var sentinelFrame = AlignDown(stackTop - 0x20, 16); + var seedRsp = sentinelFrame - sizeof(ulong); + if (!context.TryWriteUInt64(sentinelFrame, 0) || + !context.TryWriteUInt64(sentinelFrame + sizeof(ulong), 0) || + !context.TryWriteUInt64(seedRsp, 0)) + { + return false; + } + + context[CpuRegister.Rbp] = sentinelFrame; + context[CpuRegister.Rsp] = seedRsp; + return true; + } + private static bool InitializeProcessEntryFrame( CpuContext context, string processImageName, diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index 262c4f3..b088537 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -302,6 +302,23 @@ public sealed partial class DirectExecutionBackend _cpuContext[CpuRegister.R15] = value8; _cpuContext[CpuRegister.Rdi] = value; _cpuContext[CpuRegister.Rsi] = value2; + if (GuestThreadExecution.TryConsumeCurrentEntryExit(out var exitStatus, out var exitReason)) + { + if (TryCompleteGuestEntryToHostStub(argPackPtr, num, num7, importStubEntry.Nid, exitReason, exitStatus)) + { + _cpuContext[CpuRegister.Rax] = unchecked((ulong)exitStatus); + } + else + { + LastError = $"Failed to complete guest entry after {importStubEntry.Nid}: missing host return sentinel"; + _cpuContext[CpuRegister.Rax] = 18446744071562199298uL; + } + } + if (GuestThreadExecution.TryConsumeCurrentThreadBlock(out var blockReason) && + TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason)) + { + _cpuContext[CpuRegister.Rax] = 0uL; + } if (flag || flag2 || flag3) { Console.Error.WriteLine($"[LOADER][TRACE] ImportRet#{num}: nid={importStubEntry.Nid} result={orbisGen2Result} rax=0x{_cpuContext[CpuRegister.Rax]:X16}"); @@ -342,6 +359,49 @@ public sealed partial class DirectExecutionBackend return true; } + private unsafe bool TryCompleteGuestEntryToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid, string reason, int status) + { + ulong hostExit = _entryReturnSentinelRip; + if (hostExit < 65536) + { + return false; + } + try + { + *(ulong*)(argPackPtr + 96) = hostExit; + } + catch + { + return false; + } + Console.Error.WriteLine( + $"[LOADER][INFO] Guest entry exit at import#{dispatchIndex}: nid={nid} ret=0x{returnRip:X16} reason={reason} status={status}"); + return true; + } + + private unsafe bool TryYieldGuestThreadToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid, string reason) + { + ulong hostExit = _entryReturnSentinelRip; + if (hostExit < 65536) + { + return false; + } + try + { + *(ulong*)(argPackPtr + 96) = hostExit; + } + catch + { + return false; + } + + _guestThreadYieldRequested = true; + _guestThreadYieldReason = string.IsNullOrWhiteSpace(reason) ? nid : reason; + Console.Error.WriteLine( + $"[LOADER][INFO] Guest thread yield at import#{dispatchIndex}: nid={nid} ret=0x{returnRip:X16} reason={_guestThreadYieldReason}"); + return true; + } + private bool ShouldForceGuestExitOnImportLoop(string nid, ulong returnRip, long dispatchIndex, ulong arg0, ulong arg1) { if (dispatchIndex < 1200) diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index 1d308a4..afa736a 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -7,11 +7,14 @@ using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Threading; +using SharpEmu.Core.Cpu; +using SharpEmu.Core.Loader; +using SharpEmu.Core.Memory; using SharpEmu.HLE; namespace SharpEmu.Core.Cpu.Native; -public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, IDisposable +public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, IGuestThreadScheduler, IDisposable { private const int ImportLoopHistoryLength = 2048; @@ -87,6 +90,16 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private const ulong GuestImageScanEnd = 36507222016uL; + private const ulong GuestThreadStackBaseAddress = 0x7FFF_E000_0000UL; + + private const ulong GuestThreadTlsBaseAddress = 0x7FFE_0000_0000UL; + + private const ulong GuestThreadStackSize = 0x0020_0000UL; + + private const ulong GuestThreadTlsSize = 0x0001_0000UL; + + private const ulong GuestThreadRegionStride = 0x0100_0000UL; + private const uint PAGE_EXECUTE_READWRITE = 64u; private const uint PAGE_READWRITE = 4u; @@ -105,6 +118,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private nint _tlsBaseAddress; + private nint _ownedTlsBaseAddress; + private bool _ownsTlsBaseAddress; private int _tlsPatchStubOffset; @@ -197,6 +212,52 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private readonly Dictionary _importNidHashCache = new Dictionary(StringComparer.Ordinal); + private enum GuestThreadRunState + { + Ready, + Running, + Blocked, + Exited, + Faulted, + } + + private enum GuestNativeCallExitReason + { + Returned, + Blocked, + ForcedExit, + Exception, + } + + private sealed class GuestThreadState + { + public ulong ThreadHandle { get; init; } + + public ulong EntryPoint { get; init; } + + public ulong Argument { get; init; } + + public string Name { get; init; } = string.Empty; + + public CpuContext Context { get; init; } = null!; + + public GuestThreadRunState State { get; set; } + + public string? BlockReason { get; set; } + } + + private readonly object _guestThreadGate = new object(); + + private readonly Queue _readyGuestThreads = new Queue(); + + private readonly Dictionary _guestThreads = new Dictionary(); + + private int _guestThreadPumpDepth; + + private bool _guestThreadYieldRequested; + + private string? _guestThreadYieldReason; + private bool _forcedGuestExit; private ulong _lastAvTraceRip; @@ -219,7 +280,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private nint _selfHandlePtr; - private static readonly byte[] TlsPattern = new byte[9] { 100, 72, 139, 4, 37, 0, 0, 0, 0 }; + private const int MinTlsPatchInstructionBytes = 9; private delegate ulong ImportGatewayDelegate(nint backendHandle, int importIndex, nint argPackPtr); private delegate int RawExceptionHandlerDelegate(void* exceptionInfo); @@ -336,6 +397,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I { throw new OutOfMemoryException("Failed to allocate TLS base"); } + _ownedTlsBaseAddress = _tlsBaseAddress; _ownsTlsBaseAddress = true; SeedTlsLayout(_tlsBaseAddress); _hostRspSlotStorage = (nint)VirtualAlloc(null, 4096u, 12288u, 4u); @@ -376,12 +438,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I _importLoopSignatureWriteIndex = 0; _importLoopPatternHits = 0; _importNidHashCache.Clear(); + ClearGuestThreads(); _contextualUnresolvedReturnSites.Clear(); _stallWatchdogTriggered = 0; _stallWatchdogStop = false; _patchedEa020eLookupCall = false; MarkExecutionProgress(); BindTlsBase(context); + var previousGuestThreadScheduler = GuestThreadExecution.Scheduler; + GuestThreadExecution.Scheduler = this; try { if (!SetupImportStubs(importStubs)) @@ -407,6 +472,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I } finally { + GuestThreadExecution.Scheduler = previousGuestThreadScheduler; Console.Error.WriteLine("[LOADER][INFO] === Execute END (LastError: " + (LastError ?? "null") + ") ==="); } } @@ -645,18 +711,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I } if (num != _tlsBaseAddress) { - if (_ownsTlsBaseAddress && _tlsBaseAddress != 0) - { - VirtualFree((void*)_tlsBaseAddress, 0u, 32768u); - _ownsTlsBaseAddress = false; - } _tlsBaseAddress = num; + _ownsTlsBaseAddress = _tlsBaseAddress == _ownedTlsBaseAddress; } if (_tlsBaseAddress != 0) { context.FsBase = (ulong)_tlsBaseAddress; context.GsBase = (ulong)_tlsBaseAddress; SeedTlsLayout(_tlsBaseAddress); + UpdateTlsHandlerBase(_tlsBaseAddress); } } @@ -669,6 +732,30 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I *(ulong*)(tlsBase + 96) = num; } + private unsafe void UpdateTlsHandlerBase(nint tlsBase) + { + if (_tlsHandlerAddress == 0) + { + return; + } + + uint oldProtect = default; + if (!VirtualProtect((void*)_tlsHandlerAddress, 16u, 64u, &oldProtect)) + { + return; + } + + try + { + *(long*)((byte*)_tlsHandlerAddress + 2) = tlsBase; + } + finally + { + VirtualProtect((void*)_tlsHandlerAddress, 16u, oldProtect, &oldProtect); + FlushInstructionCache(GetCurrentProcess(), (void*)_tlsHandlerAddress, 16u); + } + } + private unsafe nint CreateImportHandlerTrampoline(int importIndex) { void* ptr = VirtualAlloc(null, 192u, 12288u, 64u); @@ -966,19 +1053,19 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I uint num7 = lpBuffer.Protect & 0xFF; bool flag = lpBuffer.State == 4096 && (lpBuffer.Protect & PAGE_GUARD) == 0 && num7 != PAGE_NOACCESS; bool flag2 = num7 == PAGE_EXECUTE || num7 == 32 || num7 == 64 || num7 == PAGE_EXECUTE_WRITECOPY; - if (flag && flag2 && num6 > num5 + (ulong)TlsPattern.Length) + if (flag && flag2 && num6 > num5 + MinTlsPatchInstructionBytes) { byte* ptr = (byte*)num5; - int num8 = (int)(num6 - num5) - TlsPattern.Length; - for (int i = 0; i <= num8; i++) + int scanBytes = (int)(num6 - num5); + for (int i = 0; i <= scanBytes - MinTlsPatchInstructionBytes; i++) { nint address = (nint)(ptr + i); - if (IsPatternMatch(ptr + i, TlsPattern)) + int remainingBytes = scanBytes - i; + if (TryPatchTlsLoadInstruction(address, ptr + i, remainingBytes)) { num3++; - PatchTlsInstruction(address); } - else if (TryPatchTlsImmediateStoreInstruction(address, ptr + i)) + else if (remainingBytes >= 12 && TryPatchTlsImmediateStoreInstruction(address, ptr + i)) { num9++; } @@ -1071,12 +1158,71 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I return true; } - private unsafe void PatchTlsInstruction(nint address) + private unsafe bool TryPatchTlsLoadInstruction(nint address, byte* source, int availableLength) + { + if (availableLength < MinTlsPatchInstructionBytes) + { + return false; + } + + var offset = 0; + while (offset < availableLength && source[offset] == 0x66) + { + offset++; + } + + if (offset >= availableLength || source[offset] != 0x64) + { + return false; + } + + offset++; + if (offset >= availableLength) + { + return false; + } + + var rex = (byte)0; + if (source[offset] >= 0x40 && source[offset] <= 0x4F) + { + rex = source[offset]; + offset++; + } + + if (offset + 7 > availableLength || source[offset] != 0x8B) + { + return false; + } + + var modRm = source[offset + 1]; + var sib = source[offset + 2]; + if ((modRm >> 6) != 0 || (modRm & 7) != 4 || sib != 0x25) + { + return false; + } + + var displacement = *(int*)(source + offset + 3); + if (displacement != 0) + { + return false; + } + + var destinationRegister = ((modRm >> 3) & 7) | (((rex & 4) != 0) ? 8 : 0); + var instructionLength = offset + 7; + if (instructionLength < MinTlsPatchInstructionBytes) + { + return false; + } + + return PatchTlsLoadInstruction(address, instructionLength, destinationRegister); + } + + private unsafe bool PatchTlsLoadInstruction(nint address, int instructionLength, int destinationRegister) { uint flNewProtect = default(uint); - if (!VirtualProtect((void*)address, 9u, 64u, &flNewProtect)) + if (!VirtualProtect((void*)address, (nuint)instructionLength, 64u, &flNewProtect)) { - return; + return false; } try { @@ -1087,20 +1233,29 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I if (num3 < int.MinValue || num3 > int.MaxValue) { Console.Error.WriteLine($"[LOADER][WARNING] TLS patch out of rel32 range at 0x{address:X16}"); + return false; } - else + + *(int*)(address + 1) = (int)num3; + var offset = 5; + if (destinationRegister != 0) { - *(int*)(address + 1) = (int)num3; - *(sbyte*)(address + 5) = 72; - *(sbyte*)(address + 6) = -119; - *(sbyte*)(address + 7) = -64; - *(sbyte*)(address + 8) = -112; + *(byte*)(address + offset++) = (byte)(0x48 | (destinationRegister >= 8 ? 1 : 0)); + *(byte*)(address + offset++) = 0x89; + *(byte*)(address + offset++) = (byte)(0xC0 | (destinationRegister & 7)); } + + while (offset < instructionLength) + { + *(byte*)(address + offset++) = 0x90; + } + + return true; } finally { - VirtualProtect((void*)address, 9u, flNewProtect, &flNewProtect); - FlushInstructionCache(GetCurrentProcess(), (void*)address, 9u); + VirtualProtect((void*)address, (nuint)instructionLength, flNewProtect, &flNewProtect); + FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)instructionLength); } } @@ -1268,6 +1423,439 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I } } + public bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error) + { + error = null; + if (request.ThreadHandle == 0 || request.EntryPoint < 65536) + { + error = $"invalid thread start request: handle=0x{request.ThreadHandle:X16} entry=0x{request.EntryPoint:X16}"; + return false; + } + if (!TryCreateGuestThreadState(creatorContext, request, out var thread, out error)) + { + return false; + } + lock (_guestThreadGate) + { + _guestThreads[request.ThreadHandle] = thread; + _readyGuestThreads.Enqueue(thread); + } + Console.Error.WriteLine( + $"[LOADER][INFO] Scheduled guest thread '{thread.Name}' handle=0x{thread.ThreadHandle:X16} entry=0x{thread.EntryPoint:X16} arg=0x{thread.Argument:X16}"); + return true; + } + + public void Pump(CpuContext callerContext, string reason) + { + _ = callerContext; + if (_guestThreadPumpDepth != 0) + { + return; + } + _guestThreadPumpDepth++; + try + { + for (int i = 0; i < 8; i++) + { + GuestThreadState? thread = null; + lock (_guestThreadGate) + { + while (_readyGuestThreads.Count > 0) + { + var candidate = _readyGuestThreads.Dequeue(); + if (candidate.State == GuestThreadRunState.Ready) + { + thread = candidate; + thread.State = GuestThreadRunState.Running; + break; + } + } + } + if (thread == null) + { + return; + } + RunGuestThread(thread, reason); + } + } + finally + { + _guestThreadPumpDepth--; + } + } + + private void ClearGuestThreads() + { + lock (_guestThreadGate) + { + _readyGuestThreads.Clear(); + _guestThreads.Clear(); + } + } + + private bool TryCreateGuestThreadState(CpuContext creatorContext, GuestThreadStartRequest request, out GuestThreadState thread, out string? error) + { + thread = null!; + if (!TryGetVirtualMemory(creatorContext, out var virtualMemory)) + { + error = "creator context memory is not backed by IVirtualMemory"; + return false; + } + if (!TryMapGuestThreadRegion(virtualMemory, GuestThreadStackBaseAddress, GuestThreadStackSize, ProgramHeaderFlags.Read | ProgramHeaderFlags.Write, out var stackBase, out error)) + { + return false; + } + if (!TryMapGuestThreadRegion(virtualMemory, GuestThreadTlsBaseAddress, GuestThreadTlsSize, ProgramHeaderFlags.Read | ProgramHeaderFlags.Write, out var tlsBase, out error)) + { + return false; + } + + var trackedMemory = new TrackedCpuMemory(virtualMemory); + var context = new CpuContext(trackedMemory, creatorContext.TargetGeneration) + { + Rip = request.EntryPoint, + Rflags = 0x202, + FsBase = tlsBase, + GsBase = tlsBase, + }; + context[CpuRegister.Rsp] = stackBase + GuestThreadStackSize - sizeof(ulong); + context[CpuRegister.Rdi] = request.Argument; + context[CpuRegister.Rsi] = 0; + context[CpuRegister.Rdx] = 0; + context[CpuRegister.Rcx] = 0; + context[CpuRegister.R8] = 0; + context[CpuRegister.R9] = 0; + if (!InitializeGuestThreadFrame(context) || !InitializeGuestThreadTls(context, tlsBase)) + { + error = "failed to initialize guest thread stack/TLS"; + return false; + } + + thread = new GuestThreadState + { + ThreadHandle = request.ThreadHandle, + EntryPoint = request.EntryPoint, + Argument = request.Argument, + Name = string.IsNullOrWhiteSpace(request.Name) ? $"Thread-{request.ThreadHandle:X}" : request.Name, + Context = context, + State = GuestThreadRunState.Ready, + }; + error = null; + return true; + } + + private static bool TryGetVirtualMemory(CpuContext context, out IVirtualMemory virtualMemory) + { + if (context.Memory is IVirtualMemory directMemory) + { + virtualMemory = directMemory; + return true; + } + if (context.Memory is TrackedCpuMemory trackedMemory && trackedMemory.Inner is IVirtualMemory trackedInner) + { + virtualMemory = trackedInner; + return true; + } + + virtualMemory = null!; + return false; + } + + private static bool TryMapGuestThreadRegion( + IVirtualMemory virtualMemory, + ulong baseAddress, + ulong size, + ProgramHeaderFlags protection, + out ulong mappedBase, + out string? error) + { + for (int i = 0; i < 64; i++) + { + var candidateBase = baseAddress - ((ulong)i * GuestThreadRegionStride); + if (!IsGuestThreadRegionFree(virtualMemory, candidateBase, size)) + { + continue; + } + try + { + virtualMemory.Map( + candidateBase, + size, + fileOffset: 0, + fileData: ReadOnlySpan.Empty, + protection: protection); + mappedBase = candidateBase; + error = null; + return true; + } + catch (InvalidOperationException) + { + } + } + + mappedBase = 0; + error = $"failed to map guest thread region near 0x{baseAddress:X16}"; + return false; + } + + private static bool IsGuestThreadRegionFree(IVirtualMemory virtualMemory, ulong candidateBase, ulong size) + { + var candidateEnd = candidateBase + size; + foreach (var region in virtualMemory.SnapshotRegions()) + { + var regionStart = region.VirtualAddress; + var regionEnd = regionStart + region.MemorySize; + if (candidateBase < regionEnd && regionStart < candidateEnd) + { + return false; + } + } + + return true; + } + + private static bool InitializeGuestThreadFrame(CpuContext context) + { + var stackTop = context[CpuRegister.Rsp] + sizeof(ulong); + var sentinelFrame = AlignDown(stackTop - 0x20, 16); + var seedRsp = sentinelFrame - sizeof(ulong); + if (!context.TryWriteUInt64(sentinelFrame, 0) || + !context.TryWriteUInt64(sentinelFrame + sizeof(ulong), 0) || + !context.TryWriteUInt64(seedRsp, 0)) + { + return false; + } + + context[CpuRegister.Rbp] = sentinelFrame; + context[CpuRegister.Rsp] = seedRsp; + return true; + } + + private static bool InitializeGuestThreadTls(CpuContext context, ulong tlsBase) + { + return context.TryWriteUInt64(tlsBase + 0x00, tlsBase) && + context.TryWriteUInt64(tlsBase + 0x10, tlsBase) && + context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBABEUL); + } + + private void RunGuestThread(GuestThreadState thread, string reason) + { + Console.Error.WriteLine( + $"[LOADER][INFO] Pumping guest thread '{thread.Name}' reason={reason} entry=0x{thread.EntryPoint:X16}"); + var previousContext = _cpuContext; + var previousTlsBase = _tlsBaseAddress; + var previousOwnsTlsBase = _ownsTlsBaseAddress; + var previousForcedGuestExit = _forcedGuestExit; + var previousLastError = LastError; + var previousGuestThreadHandle = GuestThreadExecution.EnterGuestThread(thread.ThreadHandle); + try + { + _cpuContext = thread.Context; + _forcedGuestExit = false; + LastError = null; + BindTlsBase(thread.Context); + var exitReason = ExecuteGuestThreadEntry(thread.Context, thread.EntryPoint, thread.Name, out var blockReason); + lock (_guestThreadGate) + { + switch (exitReason) + { + case GuestNativeCallExitReason.Returned: + thread.State = GuestThreadRunState.Exited; + break; + case GuestNativeCallExitReason.Blocked: + thread.State = GuestThreadRunState.Blocked; + thread.BlockReason = blockReason; + break; + default: + thread.State = GuestThreadRunState.Faulted; + thread.BlockReason = blockReason; + break; + } + } + Console.Error.WriteLine( + $"[LOADER][INFO] Guest thread '{thread.Name}' state={thread.State} reason={blockReason ?? "none"}"); + } + finally + { + GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle); + _cpuContext = previousContext; + _tlsBaseAddress = previousTlsBase; + _ownsTlsBaseAddress = previousOwnsTlsBase; + if (_tlsBaseAddress != 0) + { + SeedTlsLayout(_tlsBaseAddress); + UpdateTlsHandlerBase(_tlsBaseAddress); + } + _forcedGuestExit = previousForcedGuestExit; + LastError = previousLastError; + } + } + + private unsafe GuestNativeCallExitReason ExecuteGuestThreadEntry(CpuContext context, ulong entryPoint, string name, out string? reason) + { + reason = null; + if (context[CpuRegister.Rsp] == 0) + { + reason = "guest thread stack pointer is zero"; + return GuestNativeCallExitReason.Exception; + } + void* ptr = VirtualAlloc(null, 256u, 12288u, 64u); + if (ptr == null) + { + reason = "failed to allocate executable memory for guest thread stub"; + return GuestNativeCallExitReason.Exception; + } + var previousSentinel = _entryReturnSentinelRip; + var previousHostRspSlotValue = _hostRspSlotStorage != 0 ? *(ulong*)_hostRspSlotStorage : 0; + var previousYieldRequested = _guestThreadYieldRequested; + var previousYieldReason = _guestThreadYieldReason; + try + { + byte* ptr2 = (byte*)ptr; + ulong hostRspSlot = (ulong)ptr + 224uL; + int offset = 0; + ptr2[offset++] = 83; + ptr2[offset++] = 85; + ptr2[offset++] = 87; + ptr2[offset++] = 86; + ptr2[offset++] = 73; + ptr2[offset++] = 186; + *(ulong*)(ptr2 + offset) = hostRspSlot; + offset += 8; + ptr2[offset++] = 73; + ptr2[offset++] = 137; + ptr2[offset++] = 34; + ptr2[offset++] = 72; + ptr2[offset++] = 184; + *(ulong*)(ptr2 + offset) = context[CpuRegister.Rsp]; + offset += 8; + ptr2[offset++] = 72; + ptr2[offset++] = 137; + ptr2[offset++] = 196; + ptr2[offset++] = 72; + ptr2[offset++] = 131; + ptr2[offset++] = 236; + ptr2[offset++] = 8; + ptr2[offset++] = 72; + ptr2[offset++] = 189; + *(ulong*)(ptr2 + offset) = context[CpuRegister.Rbp]; + offset += 8; + ptr2[offset++] = 72; + ptr2[offset++] = 184; + *(ulong*)(ptr2 + offset) = context[CpuRegister.Rdi]; + offset += 8; + ptr2[offset++] = 72; + ptr2[offset++] = 137; + ptr2[offset++] = 199; + ptr2[offset++] = 72; + ptr2[offset++] = 184; + *(ulong*)(ptr2 + offset) = context[CpuRegister.Rsi]; + offset += 8; + ptr2[offset++] = 72; + ptr2[offset++] = 137; + ptr2[offset++] = 198; + ptr2[offset++] = 72; + ptr2[offset++] = 184; + *(ulong*)(ptr2 + offset) = context[CpuRegister.Rdx]; + offset += 8; + ptr2[offset++] = 72; + ptr2[offset++] = 137; + ptr2[offset++] = 194; + ptr2[offset++] = 72; + ptr2[offset++] = 184; + *(ulong*)(ptr2 + offset) = context[CpuRegister.Rcx]; + offset += 8; + ptr2[offset++] = 72; + ptr2[offset++] = 137; + ptr2[offset++] = 193; + ptr2[offset++] = 72; + ptr2[offset++] = 184; + *(ulong*)(ptr2 + offset) = entryPoint; + offset += 8; + ptr2[offset++] = byte.MaxValue; + ptr2[offset++] = 208; + int sentinelOffset = offset + 4; + ptr2[offset++] = 72; + ptr2[offset++] = 131; + ptr2[offset++] = 196; + ptr2[offset++] = 8; + ptr2[offset++] = 73; + ptr2[offset++] = 186; + *(ulong*)(ptr2 + offset) = hostRspSlot; + offset += 8; + ptr2[offset++] = 73; + ptr2[offset++] = 139; + ptr2[offset++] = 34; + ptr2[offset++] = 94; + ptr2[offset++] = 95; + ptr2[offset++] = 93; + ptr2[offset++] = 91; + ptr2[offset++] = 195; + ulong sentinel = (ulong)ptr + (ulong)sentinelOffset; + _entryReturnSentinelRip = sentinel; + if (!context.TryWriteUInt64(context[CpuRegister.Rsp], sentinel)) + { + reason = $"failed to patch guest thread return sentinel at 0x{context[CpuRegister.Rsp]:X16}"; + return GuestNativeCallExitReason.Exception; + } + uint oldProtect = default(uint); + VirtualProtect(ptr, 256u, 64u, &oldProtect); + FlushInstructionCache(GetCurrentProcess(), ptr, 256u); + if (_hostRspSlotStorage != 0) + { + *(ulong*)_hostRspSlotStorage = hostRspSlot; + } + _guestThreadYieldRequested = false; + _guestThreadYieldReason = null; + try + { + var nativeReturn = Marshal.GetDelegateForFunctionPointer((nint)ptr)(); + if (_guestThreadYieldRequested) + { + reason = _guestThreadYieldReason ?? "guest thread blocked"; + return GuestNativeCallExitReason.Blocked; + } + if (_forcedGuestExit) + { + reason = LastError ?? "guest thread forced exit"; + return GuestNativeCallExitReason.ForcedExit; + } + reason = $"returned 0x{nativeReturn:X8}"; + return GuestNativeCallExitReason.Returned; + } + catch (AccessViolationException ex) + { + reason = "access violation: " + ex.Message; + return GuestNativeCallExitReason.Exception; + } + catch (Exception ex) + { + reason = ex.GetType().Name + ": " + ex.Message; + return GuestNativeCallExitReason.Exception; + } + } + finally + { + _entryReturnSentinelRip = previousSentinel; + if (_hostRspSlotStorage != 0) + { + *(ulong*)_hostRspSlotStorage = previousHostRspSlotValue; + } + _guestThreadYieldRequested = previousYieldRequested; + _guestThreadYieldReason = previousYieldReason; + VirtualFree(ptr, 0u, 32768u); + } + } + + private static ulong AlignDown(ulong value, ulong alignment) + { + if (alignment == 0) + { + return value; + } + return value & ~(alignment - 1); + } + private unsafe bool ExecuteEntry(CpuContext context, ulong entryPoint, out OrbisGen2Result result) { Console.Error.WriteLine($"[LOADER][INFO] ExecuteEntry starting at 0x{entryPoint:X16}"); @@ -1315,6 +1903,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I ptr2[num3++] = 236; ptr2[num3++] = 8; ptr2[num3++] = 72; + ptr2[num3++] = 189; + *(ulong*)(ptr2 + num3) = context[CpuRegister.Rbp]; + num3 += 8; + ptr2[num3++] = 72; ptr2[num3++] = 184; *(ulong*)(ptr2 + num3) = context[CpuRegister.Rdi]; num3 += 8; @@ -1393,6 +1985,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I { num6 = Marshal.GetDelegateForFunctionPointer((nint)ptr)(); Console.Error.WriteLine($"[LOADER][INFO] Guest returned: {num6}"); + Pump(context, "entry_return"); } catch (AccessViolationException ex) { @@ -1610,11 +2203,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I _selfHandle.Free(); _selfHandlePtr = 0; } - if (_ownsTlsBaseAddress && _tlsBaseAddress != 0) + if (_ownedTlsBaseAddress != 0) { - VirtualFree((void*)_tlsBaseAddress, 0u, 32768u); + VirtualFree((void*)_ownedTlsBaseAddress, 0u, 32768u); + _ownedTlsBaseAddress = 0; } _tlsBaseAddress = 0; + _ownsTlsBaseAddress = false; if (_tlsModuleBases.Count > 0) { foreach (var (_, num3) in _tlsModuleBases) diff --git a/src/SharpEmu.HLE/GuestThreadExecution.cs b/src/SharpEmu.HLE/GuestThreadExecution.cs new file mode 100644 index 0000000..f42de73 --- /dev/null +++ b/src/SharpEmu.HLE/GuestThreadExecution.cs @@ -0,0 +1,107 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace SharpEmu.HLE; + +public readonly record struct GuestThreadStartRequest( + ulong ThreadHandle, + ulong EntryPoint, + ulong Argument, + ulong AttributeAddress, + string Name); + +public interface IGuestThreadScheduler +{ + bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error); + + void Pump(CpuContext callerContext, string reason); +} + +public static class GuestThreadExecution +{ + [ThreadStatic] + private static ulong _currentGuestThreadHandle; + + [ThreadStatic] + private static string? _pendingBlockReason; + + [ThreadStatic] + private static bool _pendingEntryExit; + + [ThreadStatic] + private static int _pendingEntryExitStatus; + + [ThreadStatic] + private static string? _pendingEntryExitReason; + + public static IGuestThreadScheduler? Scheduler { get; set; } + + public static bool IsGuestThread => _currentGuestThreadHandle != 0; + + public static ulong CurrentGuestThreadHandle => _currentGuestThreadHandle; + + public static ulong EnterGuestThread(ulong threadHandle) + { + var previous = _currentGuestThreadHandle; + _currentGuestThreadHandle = threadHandle; + _pendingBlockReason = null; + _pendingEntryExit = false; + _pendingEntryExitStatus = 0; + _pendingEntryExitReason = null; + return previous; + } + + public static void RestoreGuestThread(ulong previousThreadHandle) + { + _currentGuestThreadHandle = previousThreadHandle; + _pendingBlockReason = null; + _pendingEntryExit = false; + _pendingEntryExitStatus = 0; + _pendingEntryExitReason = null; + } + + public static bool RequestCurrentThreadBlock(string reason) + { + if (!IsGuestThread) + { + return false; + } + + _pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason; + return true; + } + + public static bool TryConsumeCurrentThreadBlock(out string reason) + { + reason = _pendingBlockReason ?? string.Empty; + if (string.IsNullOrEmpty(reason)) + { + return false; + } + + _pendingBlockReason = null; + return true; + } + + public static void RequestCurrentEntryExit(string reason, int status) + { + _pendingEntryExit = true; + _pendingEntryExitStatus = status; + _pendingEntryExitReason = string.IsNullOrWhiteSpace(reason) ? "guest_entry_exit" : reason; + } + + public static bool TryConsumeCurrentEntryExit(out int status, out string reason) + { + status = _pendingEntryExitStatus; + reason = _pendingEntryExitReason ?? string.Empty; + if (!_pendingEntryExit) + { + return false; + } + + _pendingEntryExit = false; + _pendingEntryExitStatus = 0; + _pendingEntryExitReason = null; + return true; + } +} diff --git a/src/SharpEmu.Libs/Ampr/AmprExports.cs b/src/SharpEmu.Libs/Ampr/AmprExports.cs index 7340d39..b6af3d7 100644 --- a/src/SharpEmu.Libs/Ampr/AmprExports.cs +++ b/src/SharpEmu.Libs/Ampr/AmprExports.cs @@ -16,6 +16,9 @@ public static class AmprExports private const ulong CommandBufferAux0Offset = 0x18; private const ulong CommandBufferAux1Offset = 0x20; private const ulong ReadFileRecordSize = 0x30; + private const ulong KernelEventQueueRecordSize = 0x30; + private const uint ReadFileRecordType = 1; + private const uint KernelEventQueueRecordType = 2; private static readonly ConcurrentDictionary _commandBuffers = new(); private sealed class CommandBufferState @@ -257,6 +260,108 @@ public static class AmprExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "vWU-odnS+fU", + ExportName = "sceAmprMeasureCommandSizeReadFile", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int MeasureCommandSizeReadFile(CpuContext ctx) + { + TraceAmpr(ctx, "measure_read_file", 0, ReadFileRecordSize, 0); + ctx[CpuRegister.Rax] = ReadFileRecordSize; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "sSAUCCU1dv4", + ExportName = "sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int MeasureCommandSizeWriteKernelEventQueue0400(CpuContext ctx) + { + TraceAmpr(ctx, "measure_write_equeue", 0, KernelEventQueueRecordSize, 0); + ctx[CpuRegister.Rax] = KernelEventQueueRecordSize; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "tZDDEo2tE5k", + ExportName = "sceAmprCommandBufferGetSize", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int CommandBufferGetSize(CpuContext ctx) + { + var commandBuffer = ctx[CpuRegister.Rdi]; + if (commandBuffer == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out var size, out _)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + TraceAmpr(ctx, "get_size", commandBuffer, size, 0); + ctx[CpuRegister.Rax] = size; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "GnxKOHEawhk", + ExportName = "sceAmprCommandBufferGetCurrentOffset", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int CommandBufferGetCurrentOffset(CpuContext ctx) + { + var commandBuffer = ctx[CpuRegister.Rdi]; + if (commandBuffer == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + if (!TryGetCommandBufferOffset(ctx, commandBuffer, out var offset)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + TraceAmpr(ctx, "get_offset", commandBuffer, offset, 0); + ctx[CpuRegister.Rax] = offset; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "H896Pt-yB4I", + ExportName = "sceAmprCommandBufferWriteKernelEventQueue_04_00", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int CommandBufferWriteKernelEventQueue0400(CpuContext ctx) + { + var commandBuffer = ctx[CpuRegister.Rdi]; + var equeue = ctx[CpuRegister.Rsi]; + var ident = ctx[CpuRegister.Rdx]; + var filter = ctx[CpuRegister.Rcx]; + var userData = ctx[CpuRegister.R8]; + var data = ctx[CpuRegister.R9]; + + if (commandBuffer == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + var extra = 0UL; + _ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong), out extra); + + if (!AppendKernelEventQueueRecord(ctx, commandBuffer, equeue, ident, filter, userData, data, extra)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + TraceAmpr(ctx, "write_equeue", commandBuffer, equeue, ident); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + private static bool InitializeCommandBuffer( CpuContext ctx, ulong commandBuffer, @@ -350,6 +455,22 @@ public static class AmprExports return false; } + private static bool TryGetCommandBufferOffset(CpuContext ctx, ulong commandBuffer, out ulong offset) + { + if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null) + { + offset = 0; + return false; + } + + lock (state) + { + offset = state.WriteOffset; + } + + return true; + } + private static int TryReadFileToGuestMemory( CpuContext ctx, string hostPath, @@ -420,23 +541,54 @@ public static class AmprExports ulong fileOffset, ulong bytesRead) { - if (!TryGetCommandBufferState(ctx, commandBuffer, out var buffer, out var capacity, out var state) || state is null) - { - return false; - } - Span record = stackalloc byte[(int)ReadFileRecordSize]; record.Clear(); - BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], 1); + BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], ReadFileRecordType); BinaryPrimitives.WriteUInt32LittleEndian(record[0x04..], fileId); BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], destination); BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], size); BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], fileOffset); BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], bytesRead); + return AppendCommandBufferRecord(ctx, commandBuffer, record); + } + + private static bool AppendKernelEventQueueRecord( + CpuContext ctx, + ulong commandBuffer, + ulong equeue, + ulong ident, + ulong filter, + ulong userData, + ulong data, + ulong extra) + { + Span record = stackalloc byte[(int)KernelEventQueueRecordSize]; + record.Clear(); + BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], KernelEventQueueRecordType); + BinaryPrimitives.WriteUInt32LittleEndian(record[0x04..], unchecked((uint)filter)); + BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], equeue); + BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], ident); + BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], userData); + BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], data); + BinaryPrimitives.WriteUInt64LittleEndian(record[0x28..], extra); + + return AppendCommandBufferRecord(ctx, commandBuffer, record); + } + + private static bool AppendCommandBufferRecord(CpuContext ctx, ulong commandBuffer, ReadOnlySpan record) + { + if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null) + { + return false; + } + + var recordSize = (ulong)record.Length; lock (state) { - if (state.Buffer == 0 || state.WriteOffset + ReadFileRecordSize > state.Size) + if (state.Buffer == 0 || + state.WriteOffset > state.Size || + recordSize > state.Size - state.WriteOffset) { return false; } @@ -446,7 +598,7 @@ public static class AmprExports return false; } - state.WriteOffset += ReadFileRecordSize; + state.WriteOffset += recordSize; } return true; diff --git a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs index a724e51..06b4eb5 100644 --- a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs @@ -166,11 +166,18 @@ public static class KernelEventQueueCompatExports public static int KernelWaitEqueue(CpuContext ctx) { var outCountAddress = ctx[CpuRegister.Rcx]; + var timeoutAddress = ctx[CpuRegister.R8]; if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, 0)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } + if (timeoutAddress == 0 && GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEqueue")) + { + TraceEventQueue(ctx, "wait-block", ctx[CpuRegister.Rdi]); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + TraceEventQueue(ctx, "wait", ctx[CpuRegister.Rdi]); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } diff --git a/src/SharpEmu.Libs/Kernel/KernelExports.cs b/src/SharpEmu.Libs/Kernel/KernelExports.cs index e58e0bc..663ebb0 100644 --- a/src/SharpEmu.Libs/Kernel/KernelExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelExports.cs @@ -55,7 +55,10 @@ public static class KernelExports LibraryName = "libKernel")] public static int Exit(CpuContext ctx) { - _ = ctx; + var status = unchecked((int)ctx[CpuRegister.Rdi]); + Console.Error.WriteLine($"[LOADER][INFO] exit(status={status})"); + GuestThreadExecution.RequestCurrentEntryExit("exit", status); + ctx[CpuRegister.Rax] = unchecked((ulong)status); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -68,7 +71,8 @@ public static class KernelExports { var status = unchecked((int)ctx[CpuRegister.Rdi]); Console.Error.WriteLine($"[LOADER][INFO] catchReturnFromMain(status={status})"); - ctx[CpuRegister.Rax] = 0; + GuestThreadExecution.RequestCurrentEntryExit("catchReturnFromMain", status); + ctx[CpuRegister.Rax] = unchecked((ulong)status); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -211,6 +215,19 @@ public static class KernelExports $"[LOADER][TRACE] pthread_create: out=0x{threadIdAddress:X16} attr=0x{attrAddress:X16} entry=0x{entryAddress:X16} arg=0x{argument:X16} name_ptr=0x{nameAddress:X16} name='{name}' -> thread=0x{threadHandle:X16}"); } + var scheduler = GuestThreadExecution.Scheduler; + if (scheduler is not null && entryAddress != 0) + { + var request = new GuestThreadStartRequest(threadHandle, entryAddress, argument, attrAddress, name); + if (!scheduler.TryStartThread(ctx, request, out var error)) + { + Console.Error.WriteLine( + $"[LOADER][ERROR] pthread_create: failed to schedule guest thread '{name}' entry=0x{entryAddress:X16}: {error}"); + ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN; + } + } + ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -225,6 +242,16 @@ public static class KernelExports return PthreadCreate(ctx); } + [SysAbiExport( + Nid = "Jmi+9w9u0E4", + ExportName = "pthread_create_name_np", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixPthreadCreateNameNp(CpuContext ctx) + { + return PthreadCreate(ctx); + } + [SysAbiExport( Nid = "onNY9Byn-W8", ExportName = "scePthreadJoin", diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 4d94116..b931647 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -42,6 +42,7 @@ public static class KernelMemoryCompatExports private const int SeekCur = 1; private const int SeekEnd = 2; private const ulong DirectMemorySizeBytes = 16384UL * 1024 * 1024; + private const ulong UnsetMainDirectMemoryPoolBase = ulong.MaxValue; private const ulong FlexibleMemorySizeBytes = 448UL * 1024 * 1024; private const int OrbisVirtualQueryInfoSize = 72; private const int OrbisKernelMaximumNameLength = 32; @@ -93,6 +94,7 @@ public static class KernelMemoryCompatExports private static long _nextFileDescriptor = 2; private static ulong _nextPhysicalAddress; private static ulong _nextVirtualAddress; + private static ulong _mainDirectMemoryPoolBase = UnsetMainDirectMemoryPoolBase; private static ulong _allocatedFlexibleBytes; private static ulong _threadAtexitCountCallback; private static ulong _threadAtexitReportCallback; @@ -1298,10 +1300,12 @@ public static class KernelMemoryCompatExports var hostPath = ResolveGuestPath(guestPath); if (!TryGetAprFileSize(hostPath, out var fileSize)) { + LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found"); return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } var fileId = AmprFileRegistry.Register(guestPath, hostPath); + LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} id=0x{fileId:X8} size={fileSize}"); if (idsAddress != 0 && !TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), fileId)) @@ -1309,7 +1313,7 @@ public static class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } - if (!TryWriteUInt32Compat(ctx, sizesAddress + (i * sizeof(uint)), fileSize)) + if (!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), fileSize)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } @@ -1835,7 +1839,7 @@ public static class KernelMemoryCompatExports ulong selectedAddress; lock (_memoryGate) { - if (!TryAllocateDirectMemoryLocked(searchStart, searchEnd, length, align, memoryType, out selectedAddress)) + if (!TryAllocateDirectMemoryLocked(searchStart, searchEnd, length, align, memoryType, DirectMemorySizeBytes, out selectedAddress)) { TraceDirectMemoryCall( ctx, @@ -1904,17 +1908,42 @@ public static class KernelMemoryCompatExports ulong aligned; lock (_memoryGate) { - if (!TryAllocateDirectMemoryLocked(0, DirectMemorySizeBytes, length, effectiveAlignment, memoryType, out aligned)) + var allocationLimit = DirectMemorySizeBytes; + if (_mainDirectMemoryPoolBase != UnsetMainDirectMemoryPoolBase && + !TryAddU64(_mainDirectMemoryPoolBase, DirectMemorySizeBytes, out allocationLimit)) { - TraceDirectMemoryCall( - ctx, - "allocate_main_direct", - length, - effectiveAlignment, - memoryType, - outAddress, - result: OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN); - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN; + allocationLimit = ulong.MaxValue; + } + + if (!TryAllocateDirectMemoryLocked(0, allocationLimit, length, effectiveAlignment, memoryType, allocationLimit, out aligned)) + { + var poolBase = _mainDirectMemoryPoolBase == UnsetMainDirectMemoryPoolBase + ? AlignUp(GetDirectMemoryHighWaterMarkLocked(), effectiveAlignment) + : _mainDirectMemoryPoolBase; + + if (_mainDirectMemoryPoolBase == UnsetMainDirectMemoryPoolBase && + TryAddU64(poolBase, DirectMemorySizeBytes, out var shiftedLimit) && + TryAllocateDirectMemoryLocked(0, shiftedLimit, length, effectiveAlignment, memoryType, shiftedLimit, out aligned)) + { + _mainDirectMemoryPoolBase = poolBase; + if (ShouldTraceDirectMemory()) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] main_direct_pool: base=0x{poolBase:X16} limit=0x{shiftedLimit:X16}"); + } + } + else + { + TraceDirectMemoryCall( + ctx, + "allocate_main_direct", + length, + effectiveAlignment, + memoryType, + outAddress, + result: OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN; + } } } @@ -1986,8 +2015,11 @@ public static class KernelMemoryCompatExports var flags = ctx[CpuRegister.Rcx]; var directMemoryStart = ctx[CpuRegister.R8]; var alignment = ctx[CpuRegister.R9]; - Console.Error.WriteLine( - $"[LOADER][TRACE] map_direct: inout=0x{inOutAddressPointer:X16} len=0x{length:X16} prot=0x{protection:X8} flags=0x{flags:X16} direct=0x{directMemoryStart:X16} align=0x{alignment:X16}"); + if (ShouldTraceDirectMemory()) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] map_direct: inout=0x{inOutAddressPointer:X16} len=0x{length:X16} prot=0x{protection:X8} flags=0x{flags:X16} direct=0x{directMemoryStart:X16} align=0x{alignment:X16}"); + } if (inOutAddressPointer == 0 || length == 0) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; @@ -2018,8 +2050,11 @@ public static class KernelMemoryCompatExports { reserved = TryReserveGuestVirtualRange(ctx, desiredAddress, length, protection, out mappedAddress); } - Console.Error.WriteLine( - $"[LOADER][TRACE] map_direct reserve: requested=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} reserved={reserved} mapped=0x{mappedAddress:X16}"); + if (ShouldTraceDirectMemory()) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] map_direct reserve: requested=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} reserved={reserved} mapped=0x{mappedAddress:X16}"); + } if (!reserved) { if (mappedAddress == 0) @@ -2027,7 +2062,10 @@ public static class KernelMemoryCompatExports mappedAddress = requestedAddress != 0 ? requestedAddress : AllocateMappedGuestAddress(ctx, length, effectiveAlignment); - Console.Error.WriteLine($"[LOADER][TRACE] map_direct fallback mapped=0x{mappedAddress:X16}"); + if (ShouldTraceDirectMemory()) + { + Console.Error.WriteLine($"[LOADER][TRACE] map_direct fallback mapped=0x{mappedAddress:X16}"); + } } } @@ -3560,6 +3598,12 @@ public static class KernelMemoryCompatExports var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR"); if (!string.IsNullOrWhiteSpace(app0Root)) { + if (string.Equals(guestPath, "/app0", StringComparison.OrdinalIgnoreCase) || + string.Equals(guestPath, "app0", StringComparison.OrdinalIgnoreCase)) + { + return app0Root; + } + if (guestPath.StartsWith("/app0/", StringComparison.OrdinalIgnoreCase)) { var relative = guestPath["/app0/".Length..].Replace('/', Path.DirectorySeparatorChar); @@ -3571,6 +3615,14 @@ public static class KernelMemoryCompatExports var relative = guestPath["app0/".Length..].Replace('/', Path.DirectorySeparatorChar); return Path.Combine(app0Root, relative); } + + if (!Path.IsPathFullyQualified(guestPath) && + !guestPath.StartsWith("/", StringComparison.Ordinal) && + !guestPath.StartsWith("\\", StringComparison.Ordinal)) + { + var relative = guestPath.Replace('/', Path.DirectorySeparatorChar); + return Path.Combine(app0Root, relative); + } } return guestPath; @@ -4235,7 +4287,7 @@ public static class KernelMemoryCompatExports ulong selectedAddress = 0, OrbisGen2Result? result = null) { - if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DIRECT_MEMORY"), "1", StringComparison.Ordinal)) + if (!ShouldTraceDirectMemory()) { return; } @@ -4251,12 +4303,18 @@ public static class KernelMemoryCompatExports $"[LOADER][TRACE] {operation}: ret=0x{returnRip:X16} len=0x{length:X16} align=0x{alignment:X16} type=0x{memoryType:X8} out=0x{outAddress:X16} selected=0x{selectedAddress:X16} result={result?.ToString() ?? ""}"); } + private static bool ShouldTraceDirectMemory() + { + return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DIRECT_MEMORY"), "1", StringComparison.Ordinal); + } + private static bool TryAllocateDirectMemoryLocked( ulong searchStart, ulong searchEnd, ulong length, ulong alignment, int memoryType, + ulong allocationLimit, out ulong selectedAddress) { selectedAddress = 0; @@ -4266,7 +4324,7 @@ public static class KernelMemoryCompatExports } var effectiveAlignment = alignment == 0 ? 0x1000UL : alignment; - if (!TryFindAllocatableDirectMemoryRangeLocked(searchStart, searchEnd, length, effectiveAlignment, out var freePosition) || + if (!TryFindAllocatableDirectMemoryRangeLocked(searchStart, searchEnd, length, effectiveAlignment, allocationLimit, out var freePosition) || !TryAddU64(freePosition, length, out var endAddress)) { return false; @@ -4283,6 +4341,7 @@ public static class KernelMemoryCompatExports ulong searchEnd, ulong length, ulong alignment, + ulong allocationLimit, out ulong selectedAddress) { selectedAddress = 0; @@ -4291,7 +4350,7 @@ public static class KernelMemoryCompatExports return false; } - var effectiveEnd = Math.Min(searchEnd, DirectMemorySizeBytes); + var effectiveEnd = Math.Min(searchEnd, allocationLimit); var candidate = AlignUp(searchStart, alignment); if (candidate >= effectiveEnd) { @@ -4411,7 +4470,7 @@ public static class KernelMemoryCompatExports { if (!TryAddU64(allocation.Start, allocation.Length, out var endAddress)) { - return DirectMemorySizeBytes; + return ulong.MaxValue; } if (endAddress > highWaterMark) @@ -4420,7 +4479,7 @@ public static class KernelMemoryCompatExports } } - return Math.Min(highWaterMark, DirectMemorySizeBytes); + return highWaterMark; } private static bool TryReadHostMemory(ulong address, Span destination) @@ -4817,7 +4876,7 @@ public static class KernelMemoryCompatExports return TryWriteHostPathStat(ctx, statAddress, hostPath, isDirectory); } - private static bool TryGetAprFileSize(string hostPath, out uint size) + private static bool TryGetAprFileSize(string hostPath, out ulong size) { size = 0; try @@ -4834,7 +4893,7 @@ public static class KernelMemoryCompatExports } var length = new FileInfo(hostPath).Length; - size = length >= uint.MaxValue ? uint.MaxValue : (uint)length; + size = length < 0 ? 0UL : unchecked((ulong)length); return true; } catch diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs index 2929d68..a515a5c 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs @@ -18,6 +18,7 @@ public static class KernelPthreadCompatExports private const ulong SyntheticMutexHandleBase = 0x00006000_0000_0000; private const ulong SyntheticMutexAttrHandleBase = 0x00006001_0000_0000; private const ulong SyntheticCondHandleBase = 0x00006002_0000_0000; + private const int DefaultSpuriousCondWakeMilliseconds = 1; private static readonly object _stateGate = new(); private static readonly Dictionary _mutexStates = new(); @@ -887,6 +888,7 @@ public static class KernelPthreadCompatExports } var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK; + var spuriousWake = false; lock (state.SyncRoot) { state.Waiters++; @@ -901,11 +903,36 @@ public static class KernelPthreadCompatExports return unlockResult; } + var scheduler = GuestThreadExecution.Scheduler; + if (!timed && GuestThreadExecution.RequestCurrentThreadBlock("pthread_cond_wait")) + { + TracePthreadCond("wait-block", condAddress, mutexAddress, state, timed, waitResult); + return waitResult; + } + + if (scheduler is not null) + { + Monitor.Exit(state.SyncRoot); + try + { + scheduler.Pump(ctx, "pthread_cond_wait"); + } + finally + { + Monitor.Enter(state.SyncRoot); + } + } + while (state.SignalEpoch == observedEpoch) { if (!timed) { - Monitor.Wait(state.SyncRoot); + if (!Monitor.Wait(state.SyncRoot, GetCondSpuriousWakeTimeout())) + { + spuriousWake = true; + break; + } + continue; } @@ -917,7 +944,15 @@ public static class KernelPthreadCompatExports } state.Waiters = Math.Max(0, state.Waiters - 1); - TracePthreadCond(waitResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? "wait-wake" : "wait-timeout", condAddress, mutexAddress, state, timed, waitResult); + TracePthreadCond( + spuriousWake + ? "wait-spurious" + : (waitResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? "wait-wake" : "wait-timeout"), + condAddress, + mutexAddress, + state, + timed, + waitResult); } var lockResult = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false); @@ -927,7 +962,15 @@ public static class KernelPthreadCompatExports return lockResult; } - TracePthreadCond(waitResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? "wait-exit" : "wait-exit-timeout", condAddress, mutexAddress, state, timed, waitResult); + TracePthreadCond( + spuriousWake + ? "wait-exit-spurious" + : (waitResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? "wait-exit" : "wait-exit-timeout"), + condAddress, + mutexAddress, + state, + timed, + waitResult); return waitResult; } @@ -974,6 +1017,16 @@ public static class KernelPthreadCompatExports return TimeSpan.FromTicks((long)timeoutUsec * 10L); } + private static TimeSpan GetCondSpuriousWakeTimeout() + { + if (int.TryParse(Environment.GetEnvironmentVariable("SHARPEMU_PTHREAD_COND_SPURIOUS_WAKE_MS"), out var milliseconds)) + { + return TimeSpan.FromMilliseconds(Math.Max(1, milliseconds)); + } + + return TimeSpan.FromMilliseconds(DefaultSpuriousCondWakeMilliseconds); + } + private static int NormalizeMutexType(int type) { return type switch diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs index 7556d40..72ad5f0 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs @@ -20,6 +20,7 @@ public static class KernelPthreadExtendedCompatExports private const int DefaultSchedPolicy = 0; private const int DefaultSchedPriority = 0; private const ulong SyntheticRwlockHandleBase = 0x00006003_0000_0000; + private const ulong SyntheticPthreadAttrHandleBase = 0x00006004_0000_0000; private static readonly object _stateGate = new(); private static readonly Dictionary _threadStates = new(); @@ -28,6 +29,7 @@ public static class KernelPthreadExtendedCompatExports private static readonly Dictionary _tlsKeys = new(); private static int _nextTlsKey = 1; private static long _nextSyntheticRwlockHandleId = 1; + private static long _nextSyntheticPthreadAttrHandleId = 1; private static readonly Dictionary> _threadLocalSpecific = new(); @@ -235,6 +237,41 @@ public static class KernelPthreadExtendedCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "Xs9hdiD7sAA", + ExportName = "pthread_setschedparam", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixPthreadSetschedparam(CpuContext ctx) + { + var thread = ctx[CpuRegister.Rdi]; + var policy = unchecked((int)ctx[CpuRegister.Rsi]); + var schedParamAddress = ctx[CpuRegister.Rdx]; + if (thread == 0 || schedParamAddress == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + lock (_stateGate) + { + var state = GetOrCreateThreadStateLocked(thread); + state.Priority = schedPriority; + state.Attributes = state.Attributes with + { + SchedPolicy = policy, + SchedPriority = schedPriority, + }; + } + + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "nsYoNRywwNg", ExportName = "scePthreadAttrInit", @@ -248,15 +285,32 @@ public static class KernelPthreadExtendedCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } + var syntheticHandle = AllocateSyntheticHandle(SyntheticPthreadAttrHandleBase, ref _nextSyntheticPthreadAttrHandleId); + if (!ctx.TryWriteUInt64(attrAddress, syntheticHandle)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + lock (_stateGate) { _attrStates[attrAddress] = PthreadAttrState.Default; + _attrStates[syntheticHandle] = PthreadAttrState.Default; } ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "wtkt-teR1so", + ExportName = "pthread_attr_init", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixPthreadAttrInit(CpuContext ctx) + { + return PthreadAttrInit(ctx); + } + [SysAbiExport( Nid = "62KCwEMmzcM", ExportName = "scePthreadAttrDestroy", @@ -270,15 +324,31 @@ public static class KernelPthreadExtendedCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } + var resolvedAddress = ResolvePthreadAttrHandle(ctx, attrAddress); lock (_stateGate) { _attrStates.Remove(attrAddress); + if (resolvedAddress != attrAddress) + { + _attrStates.Remove(resolvedAddress); + } } + _ = ctx.TryWriteUInt64(attrAddress, 0); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "zHchY8ft5pk", + ExportName = "pthread_attr_destroy", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixPthreadAttrDestroy(CpuContext ctx) + { + return PthreadAttrDestroy(ctx); + } + [SysAbiExport( Nid = "x1X76arYMxU", ExportName = "scePthreadAttrGet", @@ -611,16 +681,32 @@ public static class KernelPthreadExtendedCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } + var resolvedAddress = ResolvePthreadAttrHandle(ctx, attrAddress); lock (_stateGate) { - var state = GetOrCreateAttrStateLocked(attrAddress); - _attrStates[attrAddress] = state with { StackSize = stackSize }; + var state = GetOrCreateAttrStateLocked(resolvedAddress); + var updated = state with { StackSize = stackSize }; + _attrStates[resolvedAddress] = updated; + if (resolvedAddress != attrAddress) + { + _attrStates[attrAddress] = updated; + } } ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "2Q0z6rnBrTE", + ExportName = "pthread_attr_setstacksize", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixPthreadAttrSetstacksize(CpuContext ctx) + { + return PthreadAttrSetstacksize(ctx); + } + [SysAbiExport( Nid = "6ULAa0fq4jA", ExportName = "scePthreadRwlockInit", @@ -1131,6 +1217,35 @@ public static class KernelPthreadExtendedCompatExports return state; } + private static ulong ResolvePthreadAttrHandle(CpuContext ctx, ulong attrAddress) + { + if (attrAddress == 0) + { + return 0; + } + + lock (_stateGate) + { + if (_attrStates.ContainsKey(attrAddress)) + { + return attrAddress; + } + } + + if (ctx.TryReadUInt64(attrAddress, out var pointedHandle) && pointedHandle != 0) + { + lock (_stateGate) + { + if (_attrStates.ContainsKey(pointedHandle)) + { + return pointedHandle; + } + } + } + + return attrAddress; + } + private static bool TryWriteFixedUtf8CString(CpuContext ctx, ulong address, string value, int maxBytes) { if (maxBytes <= 0) diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs index 3b65ee1..edfe4d4 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs @@ -3,6 +3,7 @@ using System.Runtime.InteropServices; using System.Threading; +using SharpEmu.HLE; namespace SharpEmu.Libs.Kernel; @@ -25,12 +26,24 @@ internal static class KernelPthreadState internal static ulong GetCurrentThreadHandle() { + var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle; + if (guestThreadHandle != 0 && TryGetThreadIdentity(guestThreadHandle, out _)) + { + return guestThreadHandle; + } + EnsureCurrentThreadRegistered(); return _currentThreadHandle; } internal static ulong GetCurrentThreadUniqueId() { + var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle; + if (guestThreadHandle != 0 && TryGetThreadIdentity(guestThreadHandle, out var identity)) + { + return identity.UniqueId; + } + EnsureCurrentThreadRegistered(); return _currentThreadUniqueId; }