diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index befc6ce..0ab8d6d 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -1076,7 +1076,9 @@ public sealed partial class DirectExecutionBackend } private static bool IsImportLoopGuardBoundary(string nid) => - string.Equals(nid, "1jfXLRVzisc", StringComparison.Ordinal); + string.Equals(nid, "1jfXLRVzisc", StringComparison.Ordinal) || + string.Equals(nid, "Zxa0VhQVTsk", StringComparison.Ordinal) || + string.Equals(nid, "T72hz6ffq08", StringComparison.Ordinal); private void ResetImportLoopPattern() { diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index a154682..fa5910e 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -3480,6 +3480,18 @@ public static class KernelMemoryCompatExports return true; } + internal static bool TryFormatStringFromVaList(CpuContext ctx, string format, ulong vaListAddress, out string rendered) + { + if (!TryCreateVaListCursor(ctx, vaListAddress, out var vaCursor)) + { + rendered = format; + return false; + } + + rendered = FormatString(ctx, format, ref vaCursor); + return true; + } + private static int WriteSnprintfOutput( CpuContext ctx, ulong destination, diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs index 8446b26..fb2cfe6 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs @@ -137,7 +137,7 @@ public static class KernelPthreadCompatExports LibraryName = "libKernel")] public static int PthreadYield(CpuContext ctx) { - _ = ctx; + GuestThreadExecution.Scheduler?.Pump(ctx, "scePthreadYield"); Thread.Yield(); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } diff --git a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs index 5cd41c8..178c205 100644 --- a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs @@ -85,22 +85,29 @@ public static class KernelRuntimeCompatExports ExportName = "sceKernelNanosleep", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int KernelNanosleep(CpuContext ctx) + public static int KernelNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: false); + + [SysAbiExport( + Nid = "yS8U2TGCe1A", + ExportName = "nanosleep", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: true); + + private static int NanosleepCore(CpuContext ctx, bool posix) { var requestAddress = ctx[CpuRegister.Rdi]; var remainAddress = ctx[CpuRegister.Rsi]; if (requestAddress == 0) { - ctx[CpuRegister.Rax] = Einval; - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + return NanosleepFailure(ctx, posix, Einval, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } Span timespecBuffer = stackalloc byte[16]; if (!ctx.Memory.TryRead(requestAddress, timespecBuffer)) { - ctx[CpuRegister.Rax] = Efault; - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + return NanosleepFailure(ctx, posix, Efault, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } var tvSec = BinaryPrimitives.ReadInt64LittleEndian(timespecBuffer); @@ -108,8 +115,7 @@ public static class KernelRuntimeCompatExports if (tvSec < 0 || tvNsec < 0 || tvNsec >= 1_000_000_000L) { - ctx[CpuRegister.Rax] = Einval; - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + return NanosleepFailure(ctx, posix, Einval, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } if (tvSec == 0 && tvNsec == 0) @@ -119,7 +125,7 @@ public static class KernelRuntimeCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelNanosleep"); + GuestThreadExecution.Scheduler?.Pump(ctx, posix ? "nanosleep" : "sceKernelNanosleep"); // TimeSpan resolution is 100 ns ticks, so sub-100 ns requests round up to // a single tick rather than collapsing to a zero-length (no-op) sleep. @@ -138,6 +144,21 @@ public static class KernelRuntimeCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + private static int NanosleepFailure(CpuContext ctx, bool posix, int errnoValue, OrbisGen2Result sceResult) + { + if (posix) + { + TrySetErrno(ctx, errnoValue); + ctx[CpuRegister.Rax] = unchecked((ulong)(-1L)); + } + else + { + ctx[CpuRegister.Rax] = unchecked((ulong)errnoValue); + } + + return (int)sceResult; + } + private static void WriteRemainingTime(CpuContext ctx, ulong remainAddress, long seconds, long nanoseconds) { if (remainAddress == 0) diff --git a/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs index 171362f..69657a0 100644 --- a/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later using System.Collections.Concurrent; +using System.Diagnostics; using SharpEmu.HLE; namespace SharpEmu.Libs.Kernel; @@ -12,6 +13,9 @@ public static class KernelSemaphoreCompatExports private static readonly ConcurrentDictionary _semaphores = new(); private static int _nextSemaphoreHandle = 1; + [ThreadStatic] + private static int _semaPollBackoffCount; + private sealed class KernelSemaphoreState { public required string Name { get; init; } @@ -28,6 +32,7 @@ public static class KernelSemaphoreCompatExports { public required int NeedCount { get; init; } public required int CancelEpochAtBlock { get; init; } + public bool Timed { get; init; } // Written and read only under the owning semaphore's Gate. public int? Result { get; set; } @@ -120,6 +125,7 @@ public static class KernelSemaphoreCompatExports return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } + var pollTimedOut = false; lock (semaphore.Gate) { if (semaphore.Count >= needCount) @@ -131,36 +137,82 @@ public static class KernelSemaphoreCompatExports if (timeoutAddress != 0) { - if (!ctx.TryReadUInt32(timeoutAddress, out _)) + if (!ctx.TryReadUInt32(timeoutAddress, out var timeoutMicros)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } - _ = ctx.TryWriteUInt32(timeoutAddress, 0); - TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); - return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT); + if (timeoutMicros == 0) + { + _ = ctx.TryWriteUInt32(timeoutAddress, 0); + TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + pollTimedOut = true; + } + else + { + var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L)); + var timedWaiter = new SemaphoreWaiter + { + NeedCount = needCount, + CancelEpochAtBlock = semaphore.CancelEpoch, + Timed = true, + }; + if (GuestThreadExecution.RequestCurrentThreadBlock( + ctx, + "sceKernelWaitSema", + GetSemaphoreWakeKey(handle), + resumeHandler: () => CompleteBlockedTimedSemaWait(ctx, semaphore, timedWaiter, timeoutAddress, deadline), + wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, timedWaiter), + blockDeadlineTimestamp: deadline)) + { + semaphore.WaitingThreads++; + TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}"); + return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); + } + + // Host-owned threads cannot park in the guest scheduler; degrade to the + // immediate-timeout poll the callers already tolerate. + _ = ctx.TryWriteUInt32(timeoutAddress, 0); + TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + pollTimedOut = true; + } } - var waiter = new SemaphoreWaiter + if (!pollTimedOut) { - NeedCount = needCount, - CancelEpochAtBlock = semaphore.CancelEpoch, - }; - if (!GuestThreadExecution.RequestCurrentThreadBlock( - ctx, - "sceKernelWaitSema", - GetSemaphoreWakeKey(handle), - resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter), - wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter))) - { - TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); - return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN); - } + var waiter = new SemaphoreWaiter + { + NeedCount = needCount, + CancelEpochAtBlock = semaphore.CancelEpoch, + }; + if (!GuestThreadExecution.RequestCurrentThreadBlock( + ctx, + "sceKernelWaitSema", + GetSemaphoreWakeKey(handle), + resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter), + wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter))) + { + TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN); + } - semaphore.WaitingThreads++; - TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); - return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); + semaphore.WaitingThreads++; + TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); + return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); + } } + + GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelWaitSema"); + if ((++_semaPollBackoffCount & 255) == 0) + { + Thread.Sleep(0); + } + else + { + Thread.Yield(); + } + + return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT); } [SysAbiExport( @@ -368,6 +420,42 @@ public static class KernelSemaphoreCompatExports } } + private static int CompleteBlockedTimedSemaWait( + CpuContext ctx, + KernelSemaphoreState semaphore, + SemaphoreWaiter waiter, + ulong timeoutAddress, + long deadlineTimestamp) + { + int result; + lock (semaphore.Gate) + { + if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter)) + { + waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT; + semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1); + TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); + } + + result = waiter.Result!.Value; + } + + if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK) + { + var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp(); + var remainingMicros = remainingTicks <= 0 + ? 0u + : (uint)Math.Min(uint.MaxValue, remainingTicks / (double)Stopwatch.Frequency * 1_000_000d); + _ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros); + } + else + { + _ = ctx.TryWriteUInt32(timeoutAddress, 0); + } + + return result; + } + private static void TraceSemaphore(string message) { if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal)) diff --git a/src/SharpEmu.Libs/LibcStdioExports.cs b/src/SharpEmu.Libs/LibcStdioExports.cs index b69e88b..b49c5db 100644 --- a/src/SharpEmu.Libs/LibcStdioExports.cs +++ b/src/SharpEmu.Libs/LibcStdioExports.cs @@ -338,6 +338,359 @@ public static class LibcStdioExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "8Q60JLJ6Rv4", + ExportName = "getc", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int Getc(CpuContext ctx) => FgetcCore(ctx); + + [SysAbiExport( + Nid = "AEuF3F2f8TA", + ExportName = "fgetc", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int Fgetc(CpuContext ctx) => FgetcCore(ctx); + + private static int FgetcCore(CpuContext ctx) + { + var handle = ctx[CpuRegister.Rdi]; + if (!_fileHandles.TryGetValue(handle, out var stream)) + { + ctx[CpuRegister.Rax] = unchecked((ulong)(-1L)); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + try + { + var value = stream.ReadByte(); + ctx[CpuRegister.Rax] = value < 0 ? unchecked((ulong)(-1L)) : (ulong)value; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + catch (IOException) + { + ctx[CpuRegister.Rax] = unchecked((ulong)(-1L)); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + } + + [SysAbiExport( + Nid = "LxcEU+ICu8U", + ExportName = "feof", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int Feof(CpuContext ctx) + { + var handle = ctx[CpuRegister.Rdi]; + var atEnd = _fileHandles.TryGetValue(handle, out var stream) && + stream.CanRead && stream.Position >= stream.Length; + ctx[CpuRegister.Rax] = atEnd ? 1UL : 0UL; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "AHxyhN96dy4", + ExportName = "ferror", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int Ferror(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "3QIPIh-GDjw", + ExportName = "rewind", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int Rewind(CpuContext ctx) + { + var handle = ctx[CpuRegister.Rdi]; + if (_fileHandles.TryGetValue(handle, out var stream)) + { + try + { + stream.Seek(0, SeekOrigin.Begin); + } + catch (IOException) + { + } + } + + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "aZK8lNei-Qw", + ExportName = "fputc", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int Fputc(CpuContext ctx) + { + var character = unchecked((byte)ctx[CpuRegister.Rdi]); + var handle = ctx[CpuRegister.Rsi]; + + if (_fileHandles.TryGetValue(handle, out var stream)) + { + try + { + stream.WriteByte(character); + } + catch (IOException) + { + ctx[CpuRegister.Rax] = unchecked((ulong)(-1L)); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + } + else + { + // Unknown streams are the bundled libc's real stdout/stderr FILE objects; + // mirror the fputs HLE and forward them to the host console. + Console.Out.Write((char)character); + } + + ctx[CpuRegister.Rax] = character; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "MpxhMh8QFro", + ExportName = "fwrite", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int Fwrite(CpuContext ctx) + { + var source = ctx[CpuRegister.Rdi]; + var elementSize = ctx[CpuRegister.Rsi]; + var elementCount = ctx[CpuRegister.Rdx]; + var handle = ctx[CpuRegister.Rcx]; + + if (elementSize == 0 || elementCount == 0) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + var knownHandle = _fileHandles.TryGetValue(handle, out var stream); + if (source == 0) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + var totalRequested = elementSize * elementCount; + var buffer = ArrayPool.Shared.Rent((int)Math.Min((ulong)ReadChunkSize, totalRequested)); + ulong totalWritten = 0; + + try + { + while (totalWritten < totalRequested) + { + var request = (int)Math.Min((ulong)buffer.Length, totalRequested - totalWritten); + if (!ctx.Memory.TryRead(source + totalWritten, buffer.AsSpan(0, request))) + { + ctx[CpuRegister.Rax] = totalWritten / elementSize; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (knownHandle) + { + stream!.Write(buffer, 0, request); + } + else + { + Console.Out.Write(System.Text.Encoding.UTF8.GetString(buffer, 0, request)); + } + + totalWritten += (ulong)request; + } + } + catch (IOException) + { + ctx[CpuRegister.Rax] = totalWritten / elementSize; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + finally + { + ArrayPool.Shared.Return(buffer); + } + + ctx[CpuRegister.Rax] = totalWritten / elementSize; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "MUjC4lbHrK4", + ExportName = "fflush", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int Fflush(CpuContext ctx) + { + var handle = ctx[CpuRegister.Rdi]; + + try + { + if (handle == 0) + { + foreach (var stream in _fileHandles.Values) + { + stream.Flush(); + } + } + else if (_fileHandles.TryGetValue(handle, out var stream)) + { + stream.Flush(); + } + } + catch (IOException) + { + ctx[CpuRegister.Rax] = unchecked((ulong)(-1L)); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "fffwELXNVFA", + ExportName = "fprintf", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int Fprintf(CpuContext ctx) + { + var handle = ctx[CpuRegister.Rdi]; + var formatAddress = ctx[CpuRegister.Rsi]; + + if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, formatAddress, MaxPathLength, out var format)) + { + ctx[CpuRegister.Rax] = unchecked((ulong)(-1L)); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + var rendered = KernelMemoryCompatExports.FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 2); + return WriteRenderedText(ctx, handle, rendered); + } + + [SysAbiExport( + Nid = "pDBDcY6uLSA", + ExportName = "vfprintf", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int Vfprintf(CpuContext ctx) + { + var handle = ctx[CpuRegister.Rdi]; + var formatAddress = ctx[CpuRegister.Rsi]; + var vaListAddress = ctx[CpuRegister.Rdx]; + + if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, formatAddress, MaxPathLength, out var format)) + { + ctx[CpuRegister.Rax] = unchecked((ulong)(-1L)); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + KernelMemoryCompatExports.TryFormatStringFromVaList(ctx, format, vaListAddress, out var rendered); + return WriteRenderedText(ctx, handle, rendered); + } + + private static int WriteRenderedText(CpuContext ctx, ulong handle, string rendered) + { + var payload = System.Text.Encoding.UTF8.GetBytes(rendered); + + if (_fileHandles.TryGetValue(handle, out var stream)) + { + try + { + stream.Write(payload, 0, payload.Length); + } + catch (IOException) + { + ctx[CpuRegister.Rax] = unchecked((ulong)(-1L)); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + } + else + { + Console.Out.Write(rendered); + } + + ctx[CpuRegister.Rax] = (ulong)payload.Length; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "gkWgn0p1AfU", + ExportName = "freopen", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int Freopen(CpuContext ctx) + { + var pathAddress = ctx[CpuRegister.Rdi]; + var modeAddress = ctx[CpuRegister.Rsi]; + var handle = ctx[CpuRegister.Rdx]; + + if (pathAddress == 0 || modeAddress == 0 || handle == 0 || + !KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, pathAddress, MaxPathLength, out var guestPath) || + !KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, modeAddress, MaxModeLength, out var mode) || + !TryParseFopenMode(mode, out var fileMode, out var fileAccess)) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + if (_fileHandles.TryRemove(handle, out var previousStream)) + { + previousStream.Dispose(); + } + + var hostPath = KernelMemoryCompatExports.ResolveGuestPath(guestPath); + if (fileAccess != FileAccess.Read && KernelMemoryCompatExports.IsReadOnlyGuestMutationPath(guestPath)) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED; + } + + try + { + if (fileAccess != FileAccess.Read) + { + var parentDirectory = Path.GetDirectoryName(hostPath); + if (!string.IsNullOrWhiteSpace(parentDirectory)) + { + Directory.CreateDirectory(parentDirectory); + } + } + + var stream = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite); + // freopen keeps the caller's FILE* identity, so rebind the same handle value. + _fileHandles[handle] = stream; + + if (_traceStdio) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] freopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> OK handle=0x{handle:X}"); + } + + ctx[CpuRegister.Rax] = handle; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + if (_traceStdio) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] freopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> FAILED {ex.GetType().Name}: {ex.Message}"); + } + + ctx[CpuRegister.Rax] = 0; + return ex is UnauthorizedAccessException + ? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + } + [SysAbiExport( Nid = "sUP1hBaouOw", ExportName = "_Getpctype", diff --git a/src/SharpEmu.Libs/Np/NpManagerExports.cs b/src/SharpEmu.Libs/Np/NpManagerExports.cs index 61bccb9..3ecd42c 100644 --- a/src/SharpEmu.Libs/Np/NpManagerExports.cs +++ b/src/SharpEmu.Libs/Np/NpManagerExports.cs @@ -22,6 +22,17 @@ public static class NpManagerExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "S7QTn72PrDw", + ExportName = "sceNpDeleteRequest", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNpManager")] + public static int NpDeleteRequest(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "JELHf4xPufo", ExportName = "sceNpCheckCallbackForLib", diff --git a/src/SharpEmu.Libs/Pad/PadExports.cs b/src/SharpEmu.Libs/Pad/PadExports.cs index 459de27..3f01e52 100644 --- a/src/SharpEmu.Libs/Pad/PadExports.cs +++ b/src/SharpEmu.Libs/Pad/PadExports.cs @@ -178,6 +178,16 @@ public static class PadExports return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK); } + [SysAbiExport( + Nid = "2JgFB2n9oUM", + ExportName = "scePadSetTriggerEffect", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePad")] + public static int PadSetTriggerEffect(CpuContext ctx) + { + return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK); + } + [SysAbiExport( Nid = "yFVnOdGxvZY", ExportName = "scePadSetVibration",