diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index e6825589..8e084c7b 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -723,6 +723,7 @@ public sealed partial class DirectExecutionBackend "Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen "TywrFKCoLGY" or // sceSaveDataInitialize3 "dyIhnXq-0SM" or // sceSaveDataDirNameSearch + "ZP4e7rlzOUk" or // sceSaveDataMount3 "ERKzksauAJA" or // sceSaveDataDialogGetStatus "KK3Bdg1RWK0" or // sceSaveDataDialogUpdateStatus "en7gNVnh878" or // sceSaveDataDialogIsReadyToDisplay diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index 9e719f4d..b3e56f65 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -355,6 +355,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I public GuestThreadRunState State { get; set; } + public ulong ExitValue { get; set; } + public string? BlockReason { get; set; } public bool HasBlockedContinuation { get; set; } @@ -1266,7 +1268,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I libraryName.IndexOf("Kernel", StringComparison.OrdinalIgnoreCase) >= 0; } - private static bool PreferLleForLibcExport(string exportName) + private bool PreferLleForLibcExport(string exportName) { if (string.IsNullOrWhiteSpace(exportName)) { @@ -1287,6 +1289,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I { return true; } + if (IsLibcAllocatorExport(exportName)) + { + return CanUseLleLibcAllocatorFamily(); + } if (string.Equals(value, "0", StringComparison.Ordinal)) { return true; @@ -1298,6 +1304,51 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I return IsSafeLleLibcExport(exportName); } + private bool CanUseLleLibcAllocatorFamily() + { + return HasUsableLleLibcExport("gQX+4GDQjpM", "malloc") && + HasUsableLleLibcExport("tIhsqj0qsFE", "free") && + HasUsableLleLibcExport("2X5agFjKxMc", "calloc") && + HasUsableLleLibcExport("Y7aJ1uydPMo", "realloc") && + HasUsableLleLibcExport("Ujf3KzMvRmI", "memalign") && + HasUsableLleLibcExport("2Btkg8k24Zg", "aligned_alloc") && + HasUsableLleLibcExport("cVSk9y8URbc", "posix_memalign"); + } + + private bool HasUsableLleLibcExport(string nid, string exportName) + { + if (TryResolveRuntimeSymbolAddress(nid, out var address) && IsDirectImportTargetUsable(address)) + { + return true; + } + + foreach (var candidate in EnumerateRuntimeSymbolCandidates(exportName)) + { + if (TryResolveRuntimeSymbolAddress(candidate, out address) && IsDirectImportTargetUsable(address)) + { + return true; + } + } + + return false; + } + + private static bool IsLibcAllocatorExport(string exportName) + { + return exportName switch + { + "malloc" or + "free" or + "calloc" or + "realloc" or + "memalign" or + "aligned_alloc" or + "posix_memalign" or + "malloc_usable_size" => true, + _ => false, + }; + } + private static bool IsSafeLleLibcExport(string exportName) { return exportName switch @@ -2406,6 +2457,69 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I public bool SupportsGuestContextTransfer => true; + public bool TryJoinThread( + CpuContext callerContext, + ulong threadHandle, + out ulong returnValue, + out string? error) + { + returnValue = 0; + error = null; + if (threadHandle == 0) + { + error = "thread handle is zero"; + return false; + } + + if (threadHandle == GuestThreadExecution.CurrentGuestThreadHandle) + { + error = "thread cannot join itself"; + return false; + } + + while (!ActiveForcedGuestExit) + { + Thread? hostThread; + lock (_guestThreadGate) + { + if (!_guestThreads.TryGetValue(threadHandle, out var thread)) + { + error = $"unknown guest thread 0x{threadHandle:X16}"; + return false; + } + + if (thread.State == GuestThreadRunState.Exited) + { + returnValue = thread.ExitValue; + return true; + } + + if (thread.State == GuestThreadRunState.Faulted) + { + error = + $"guest thread 0x{threadHandle:X16} faulted: " + + (thread.BlockReason ?? "unknown error"); + return false; + } + + hostThread = thread.HostThread; + } + + if (hostThread is not null && + !ReferenceEquals(hostThread, Thread.CurrentThread)) + { + hostThread.Join(1); + } + else + { + Thread.Sleep(1); + } + } + + error = "guest execution stopped while joining thread"; + return false; + } + public void Pump(CpuContext callerContext, string reason) { _ = callerContext; @@ -3254,6 +3368,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I switch (exitReason) { case GuestNativeCallExitReason.Returned: + thread.ExitValue = thread.Context[CpuRegister.Rax]; thread.State = GuestThreadRunState.Exited; break; case GuestNativeCallExitReason.Blocked: diff --git a/src/SharpEmu.HLE/GuestThreadExecution.cs b/src/SharpEmu.HLE/GuestThreadExecution.cs index 28e6efa0..6d572208 100644 --- a/src/SharpEmu.HLE/GuestThreadExecution.cs +++ b/src/SharpEmu.HLE/GuestThreadExecution.cs @@ -29,6 +29,12 @@ public interface IGuestThreadScheduler bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error); + bool TryJoinThread( + CpuContext callerContext, + ulong threadHandle, + out ulong returnValue, + out string? error); + void Pump(CpuContext callerContext, string reason); int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue); diff --git a/src/SharpEmu.Libs/Kernel/KernelExports.cs b/src/SharpEmu.Libs/Kernel/KernelExports.cs index 6f2c709c..93d920f7 100644 --- a/src/SharpEmu.Libs/Kernel/KernelExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelExports.cs @@ -306,10 +306,6 @@ public static class KernelExports { var threadId = ctx[CpuRegister.Rdi]; var returnValueAddress = ctx[CpuRegister.Rsi]; - if (returnValueAddress != 0 && !ctx.TryWriteUInt64(returnValueAddress, 0)) - { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; - } if (ShouldTracePthread()) { @@ -317,6 +313,30 @@ public static class KernelExports $"[LOADER][TRACE] pthread_join: thread=0x{threadId:X16} retval_out=0x{returnValueAddress:X16}"); } + var returnValue = 0UL; + if (GuestThreadExecution.Scheduler is { } scheduler && + !scheduler.TryJoinThread(ctx, threadId, out returnValue, out var error)) + { + Console.Error.WriteLine( + $"[LOADER][ERROR] pthread_join: thread=0x{threadId:X16}: {error}"); + var result = string.Equals( + error, + "thread cannot join itself", + StringComparison.Ordinal) + ? OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT + : OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + ctx[CpuRegister.Rax] = unchecked((ulong)(int)result); + return (int)result; + } + + if (returnValueAddress != 0 && + !ctx.TryWriteUInt64(returnValueAddress, returnValue)) + { + ctx[CpuRegister.Rax] = + unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 7a358002..9982448a 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -99,10 +99,12 @@ public static class KernelMemoryCompatExports private static readonly object _tlsGate = new(); private static readonly object _ioTraceGate = new(); private static readonly object _statCacheGate = new(); + private static readonly object _guestMountGate = new(); private static readonly Dictionary _directAllocations = new(); private static readonly Dictionary _libcAllocations = new(); private static readonly Dictionary _mappedRegions = new(); private static readonly Dictionary _tlsModuleBlocks = new(); + private static readonly Dictionary _guestMounts = new(StringComparer.OrdinalIgnoreCase); private static readonly HashSet _tracedStatResults = new(StringComparer.Ordinal); private static readonly HashSet _negativeStatCache = new(StringComparer.OrdinalIgnoreCase); private static long _nextFileDescriptor = 2; @@ -153,6 +155,32 @@ public static class KernelMemoryCompatExports 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); + public static void RegisterGuestPathMount(string guestMountPoint, string hostRoot) + { + ArgumentException.ThrowIfNullOrWhiteSpace(guestMountPoint); + ArgumentException.ThrowIfNullOrWhiteSpace(hostRoot); + + var normalizedMountPoint = NormalizeGuestStatCachePath(guestMountPoint); + if (normalizedMountPoint is null || normalizedMountPoint == "/") + { + throw new ArgumentException("Guest mount point must name a directory.", nameof(guestMountPoint)); + } + + var normalizedHostRoot = Path.GetFullPath(hostRoot); + Directory.CreateDirectory(normalizedHostRoot); + lock (_guestMountGate) + { + _guestMounts[normalizedMountPoint] = normalizedHostRoot; + } + + lock (_statCacheGate) + { + _negativeStatCache.RemoveWhere(path => + string.Equals(path, normalizedMountPoint, StringComparison.OrdinalIgnoreCase) || + path.StartsWith(normalizedMountPoint + "/", StringComparison.OrdinalIgnoreCase)); + } + } + internal static bool TryAllocateHleData( CpuContext ctx, ulong length, @@ -4033,6 +4061,11 @@ public static class KernelMemoryCompatExports return guestPath; } + if (TryResolveRegisteredGuestMount(guestPath, out var mountedPath)) + { + return mountedPath; + } + if (guestPath.StartsWith("/devlog/app/", StringComparison.OrdinalIgnoreCase)) { var relative = NormalizeMountRelativePath(guestPath["/devlog/app/".Length..]); @@ -4131,6 +4164,51 @@ public static class KernelMemoryCompatExports return guestPath; } + private static bool TryResolveRegisteredGuestMount(string guestPath, out string hostPath) + { + hostPath = string.Empty; + var normalizedGuestPath = NormalizeGuestStatCachePath(guestPath); + if (normalizedGuestPath is null) + { + return false; + } + + string? matchedMountPoint = null; + string? matchedHostRoot = null; + lock (_guestMountGate) + { + foreach (var (mountPoint, hostRoot) in _guestMounts) + { + if ((string.Equals(normalizedGuestPath, mountPoint, StringComparison.OrdinalIgnoreCase) || + normalizedGuestPath.StartsWith(mountPoint + "/", StringComparison.OrdinalIgnoreCase)) && + (matchedMountPoint is null || mountPoint.Length > matchedMountPoint.Length)) + { + matchedMountPoint = mountPoint; + matchedHostRoot = hostRoot; + } + } + } + + if (matchedMountPoint is null || matchedHostRoot is null) + { + return false; + } + + var relativePath = normalizedGuestPath[matchedMountPoint.Length..].TrimStart('/'); + var candidate = Path.GetFullPath(Path.Combine( + matchedHostRoot, + NormalizeMountRelativePath(relativePath))); + var rootWithSeparator = Path.TrimEndingDirectorySeparator(matchedHostRoot) + Path.DirectorySeparatorChar; + if (!string.Equals(candidate, matchedHostRoot, StringComparison.OrdinalIgnoreCase) && + !candidate.StartsWith(rootWithSeparator, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + hostPath = candidate; + return true; + } + private static string? ResolveApp0Root() { var cached = Volatile.Read(ref _cachedApp0Root);