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 73acd159..124af768 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); diff --git a/src/SharpEmu.Libs/SaveData/SaveDataExports.cs b/src/SharpEmu.Libs/SaveData/SaveDataExports.cs index 4af371bc..359c53e6 100644 --- a/src/SharpEmu.Libs/SaveData/SaveDataExports.cs +++ b/src/SharpEmu.Libs/SaveData/SaveDataExports.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.HLE; +using SharpEmu.Libs.Kernel; using System.Buffers.Binary; using System.Text; @@ -10,6 +11,8 @@ namespace SharpEmu.Libs.SaveData; public static class SaveDataExports { private const int OrbisSaveDataErrorParameter = unchecked((int)0x809F0000); + private const int OrbisSaveDataErrorExists = unchecked((int)0x809F0007); + private const int OrbisSaveDataErrorNotFound = unchecked((int)0x809F0008); private const int OrbisSaveDataErrorInternal = unchecked((int)0x809F000B); private const int SaveDataTitleIdSize = 10; private const int SaveDataDirNameSize = 32; @@ -23,6 +26,9 @@ public static class SaveDataExports private const ulong ResultInfosOffset = 0x20; private const uint SortKeyFreeBlocks = 5; private const uint SortOrderDescent = 1; + private const uint MountModeCreate = 1u << 2; + private const uint MountModeCreate2 = 1u << 5; + private const int MountResultSize = 0x40; private static readonly object _stateGate = new(); private static string? _titleId; @@ -149,6 +155,95 @@ public static class SaveDataExports } } + [SysAbiExport( + Nid = "ZP4e7rlzOUk", + ExportName = "sceSaveDataMount3", + Target = Generation.Gen5, + LibraryName = "libSceSaveData")] + public static int SaveDataMount3(CpuContext ctx) + { + var mountAddress = ctx[CpuRegister.Rdi]; + var resultAddress = ctx[CpuRegister.Rsi]; + if (mountAddress == 0 || resultAddress == 0) + { + return SetReturn(ctx, OrbisSaveDataErrorParameter); + } + + if (!TryReadInt32(ctx, mountAddress, out var userId) || + !ctx.TryReadUInt64(mountAddress + 0x08, out var dirNameAddress) || + !ctx.TryReadUInt64(mountAddress + 0x10, out var blocks) || + !ctx.TryReadUInt64(mountAddress + 0x18, out var systemBlocks) || + !TryReadUInt32(ctx, mountAddress + 0x20, out var mountMode) || + !TryReadUInt32(ctx, mountAddress + 0x24, out var resource) || + !TryReadUInt32(ctx, mountAddress + 0x28, out var mode) || + dirNameAddress == 0 || + !TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName)) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + if (userId < 0 || string.IsNullOrWhiteSpace(dirName)) + { + return SetReturn(ctx, OrbisSaveDataErrorParameter); + } + + try + { + var titleId = ResolveConfiguredTitleId(); + var savePath = Path.Combine( + ResolveTitleSaveRoot(userId, titleId), + SanitizePathSegment(dirName)); + var existed = Directory.Exists(savePath); + var create = (mountMode & MountModeCreate) != 0; + var createIfMissing = (mountMode & MountModeCreate2) != 0; + + if (!existed && !create && !createIfMissing) + { + return SetReturn(ctx, OrbisSaveDataErrorNotFound); + } + + if (existed && create) + { + return SetReturn(ctx, OrbisSaveDataErrorExists); + } + + if (!existed) + { + Directory.CreateDirectory(savePath); + } + + const string mountPoint = "/savedata0"; + KernelMemoryCompatExports.RegisterGuestPathMount(mountPoint, savePath); + + Span result = stackalloc byte[MountResultSize]; + result.Clear(); + WriteAscii(result[..16], mountPoint); + BinaryPrimitives.WriteUInt32LittleEndian(result[0x1C..], createIfMissing && !existed ? 1u : 0u); + if (!ctx.Memory.TryWrite(resultAddress, result)) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + TraceSaveData( + $"mount3 user={userId} title={titleId} dir={dirName} blocks={blocks} " + + $"system_blocks={systemBlocks} mount_mode=0x{mountMode:X} resource={resource} mode={mode} " + + $"mount_point={mountPoint} created={!existed} root='{savePath}'"); + return SetReturn(ctx, 0); + } + catch (IOException) + { + return SetReturn(ctx, OrbisSaveDataErrorInternal); + } + catch (UnauthorizedAccessException) + { + return SetReturn(ctx, OrbisSaveDataErrorInternal); + } + catch (ArgumentException) + { + return SetReturn(ctx, OrbisSaveDataErrorParameter); + } + } + private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond) { cond = default;