diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index a79a1e26..e55e21c9 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -640,6 +640,21 @@ public static partial class AgcExports public static int GetRegisterDefaults2Internal(CpuContext ctx) => ReturnRegisterDefaults(ctx, internalDefaults: true); + /// + /// Reports that the GPU is not running in Trinity mode, matching the base + /// console this backend emulates. + /// + [SysAbiExport( + Nid = "BfBDZGbti7A", + ExportName = "sceAgcGetIsTrinityMode", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int GetIsTrinityMode(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "f3dg2CSgRKY", ExportName = "sceAgcCreateShader", diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index f4ff293a..1b5a4f47 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -2327,6 +2327,37 @@ public static partial class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "smIj7eqzZE8", + ExportName = "clock_getres", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int ClockGetres(CpuContext ctx) + { + var timespecAddress = ctx[CpuRegister.Rsi]; + + // POSIX allows a null resolution pointer: the call then only validates + // the clock id, which every id a title passes here does. + if (timespecAddress == 0) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + // clock_gettime above is backed by DateTimeOffset.UtcNow, whose tick is + // 100 ns, so that is the honest resolution to report rather than the 1 ns + // a caller might otherwise assume it can rely on. + const ulong ResolutionNanoseconds = 100; + if (!ctx.TryWriteUInt64(timespecAddress, 0) || + !ctx.TryWriteUInt64(timespecAddress + sizeof(long), ResolutionNanoseconds)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "vNe1w4diLCs", ExportName = "__tls_get_addr", @@ -3398,6 +3429,47 @@ public static partial class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } + /// + /// POSIX alias of ; identical (addr, len, prot) + /// argument order. Imported by libcohtml, whose embedded V8 changes page + /// permissions through this name when moving JIT pages between writable and + /// executable. + /// + [SysAbiExport( + Nid = "YQOfxL4QfeU", + ExportName = "mprotect", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixMprotect(CpuContext ctx) => KernelMprotect(ctx); + + /// + /// POSIX alias of ; identical (addr, len) argument + /// order. + /// + [SysAbiExport( + Nid = "UqDGjXA5yUM", + ExportName = "munmap", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixMunmap(CpuContext ctx) => KernelMunmap(ctx); + + /// + /// Reports the 16 KiB page granularity this backend maps and aligns against + /// (), not the host's 4 KiB. An allocator that + /// rounded to the host value would hand back sub-page offsets that every + /// mapping call here then rejects for misalignment. + /// + [SysAbiExport( + Nid = "k+AXqu2-eBc", + ExportName = "getpagesize", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixGetPageSize(CpuContext ctx) + { + ctx[CpuRegister.Rax] = OrbisPageSize; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "vSMAm3cxYTY", ExportName = "sceKernelMprotect", @@ -5660,6 +5732,74 @@ public static partial class KernelMemoryCompatExports return true; } + /// + /// Inserts into the tracked-region table, + /// carving it out of any regions it overlaps and preserving the parts of + /// those regions that fall outside it. + /// + /// + /// The table is a SortedList keyed by start address, so assigning directly + /// destroys any existing entry that happens to share a start address. That + /// silently discarded enclosing reservations: a title reserves a large range + /// and then commits a small mapping at the same base, and the record of + /// everything past the small mapping disappears — leaving sceKernelVirtualQuery + /// unable to find memory the guest legitimately owns. + /// + /// Carving also keeps the table non-overlapping. Previously a new region + /// starting inside an existing one produced two overlapping entries, which + /// the ordered scan in TryFindVirtualQueryRegionLocked is not written to + /// expect. + /// + private static void ReplaceMappedRegionRangeLocked(MappedRegion replacement) + { + if (replacement.Length == 0 || + !TryAddU64(replacement.Address, replacement.Length, out var replacementEnd)) + { + _mappedRegions[replacement.Address] = replacement; + return; + } + + var start = replacement.Address; + List? overlapping = null; + foreach (var region in _mappedRegions.Values) + { + if (region.Length == 0 || + !TryAddU64(region.Address, region.Length, out var regionEnd)) + { + continue; + } + + if (region.Address < replacementEnd && regionEnd > start) + { + (overlapping ??= []).Add(region); + } + } + + if (overlapping is not null) + { + foreach (var region in overlapping) + { + _mappedRegions.Remove(region.Address); + } + + foreach (var region in overlapping) + { + var regionEnd = region.Address + region.Length; + if (region.Address < start) + { + AddMappedRegionSliceLocked(region, region.Address, start, region.Protection); + } + + if (regionEnd > replacementEnd) + { + AddMappedRegionSliceLocked(region, replacementEnd, regionEnd, region.Protection); + } + } + } + + _mappedRegions[start] = replacement; + } + private static void AddMappedRegionSliceLocked( MappedRegion source, ulong start, diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs index 72e5c9de..8ec75067 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs @@ -578,6 +578,30 @@ public static class KernelPthreadCompatExports return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); } + /// + /// The POSIX-named alias of . libKernel exports the + /// same routine under two NIDs, and shipped middleware links the plain name: + /// DOOM's libcohtml, PlayFab and party modules all import this one rather + /// than scePthreadOnce. + /// + [SysAbiExport( + Nid = "Z4QosVuAsA0", + ExportName = "pthread_once", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadOncePOSIX(CpuContext ctx) => PthreadOnce(ctx); + + /// + /// The POSIX-named alias of , following the same + /// two-NID pattern as . + /// + [SysAbiExport( + Nid = "9vyP6Z7bqzc", + ExportName = "pthread_rename_np", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadRenameNpPOSIX(CpuContext ctx) => PthreadRename(ctx); + private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress) { if (mutexAddress == 0) diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs index 4785bb4c..8b104f29 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs @@ -860,6 +860,18 @@ public static class KernelPthreadExtendedCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + /// + /// The POSIX-named alias of . libKernel + /// exports the same routine under two NIDs; middleware compiled against the + /// plain POSIX headers links this one rather than scePthreadAttrGetschedparam. + /// + [SysAbiExport( + Nid = "qlk9pSLsUmM", + ExportName = "pthread_attr_getschedparam", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadAttrGetschedparamPOSIX(CpuContext ctx) => PthreadAttrGetschedparam(ctx); + [SysAbiExport( Nid = "FXPWHNk8Of0", ExportName = "scePthreadAttrGetschedparam", @@ -1133,6 +1145,90 @@ public static class KernelPthreadExtendedCompatExports LibraryName = "libKernel")] public static int PosixPthreadRwlockWrlock(CpuContext ctx) => PthreadRwlockWrlock(ctx); + [SysAbiExport( + Nid = "SFxTMOfuCkE", + ExportName = "pthread_rwlock_tryrdlock", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixPthreadRwlockTryrdlock(CpuContext ctx) => + PthreadRwlockTryLockCore(ctx, ctx[CpuRegister.Rdi], write: false); + + [SysAbiExport( + Nid = "XhWHn6P5R7U", + ExportName = "pthread_rwlock_trywrlock", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixPthreadRwlockTrywrlock(CpuContext ctx) => + PthreadRwlockTryLockCore(ctx, ctx[CpuRegister.Rdi], write: true); + + /// + /// Non-blocking counterpart of : acquires + /// only if the lock is free right now, otherwise reports BUSY. + /// + /// + /// Deliberately not routed through TryAcquireBlockedRwlock. That helper exists + /// for the scheduler resume path and decrements WaitingWriters on success, + /// which is correct only for a thread that previously incremented it. A fresh + /// try never did, so reusing it would silently consume another thread's + /// waiter count and let a queued writer be skipped. + /// + private static int PthreadRwlockTryLockCore(CpuContext ctx, ulong rwlockAddress, bool write) + { + if (rwlockAddress == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out var resolvedAddress, out var rwlock)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + var currentThreadId = KernelPthreadState.GetCurrentThreadHandle(); + lock (rwlock.SyncRoot) + { + if (write) + { + if (rwlock.WriterThreadId == currentThreadId || rwlock.GetReaderCount(currentThreadId) > 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK; + } + + // Mirrors the blocking path's re-entrant compat-writer grant so the + // two agree on what counts as already owning the lock. + if (rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId) > 0) + { + rwlock.AddCompatWriter(currentThreadId); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + if (rwlock.WriterThreadId != 0 || + rwlock.ReaderTotalCount != 0 || + rwlock.CompatWriterTotalCount != 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY; + } + + DetectRwlockWriterConflict(resolvedAddress, rwlock, currentThreadId, "trywrlock"); + rwlock.WriterThreadId = currentThreadId; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + if (rwlock.WriterThreadId == currentThreadId) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK; + } + + if (ReaderMustWaitForRwlock(rwlock, currentThreadId)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY; + } + + rwlock.AddReader(currentThreadId); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + } + [SysAbiExport( Nid = "+L98PIbGttk", ExportName = "scePthreadRwlockUnlock", @@ -1819,4 +1915,94 @@ public static class KernelPthreadExtendedCompatExports BinaryPrimitives.WriteInt32LittleEndian(bytes, value); return ctx.Memory.TryWrite(address, bytes); } + + // POSIX-named aliases. libKernel exports each of these routines under two + // NIDs -- a scePthread* name and the plain POSIX name -- and middleware + // compiled against POSIX headers links the latter. Both take identical + // arguments and, per the convention already used by scePthreadOnce's alias, + // return the same OrbisGen2Result rather than translating to errno. + + [SysAbiExport( + Nid = "a2P9wYGeZvc", + ExportName = "pthread_setprio", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadSetprioPOSIX(CpuContext ctx) => PthreadSetprio(ctx); + + [SysAbiExport( + Nid = "FIs3-UQT9sg", + ExportName = "pthread_getschedparam", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadGetschedparamPOSIX(CpuContext ctx) => PthreadGetschedparam(ctx); + + [SysAbiExport( + Nid = "vQm4fDEsWi8", + ExportName = "pthread_attr_getstack", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadAttrGetstackPOSIX(CpuContext ctx) => PthreadAttrGetstack(ctx); + + [SysAbiExport( + Nid = "Ucsu-OK+els", + ExportName = "pthread_attr_get_np", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadAttrGetNpPOSIX(CpuContext ctx) => PthreadAttrGet(ctx); + + [SysAbiExport( + Nid = "JarMIy8kKEY", + ExportName = "pthread_attr_setschedpolicy", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadAttrSetschedpolicyPOSIX(CpuContext ctx) => PthreadAttrSetschedpolicy(ctx); + + [SysAbiExport( + Nid = "E+tyo3lp5Lw", + ExportName = "pthread_attr_setdetachstate", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadAttrSetdetachstatePOSIX(CpuContext ctx) => PthreadAttrSetdetachstate(ctx); + + [SysAbiExport( + Nid = "euKRgm0Vn2M", + ExportName = "pthread_attr_setschedparam", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadAttrSetschedparamPOSIX(CpuContext ctx) => PthreadAttrSetschedparam(ctx); + + [SysAbiExport( + Nid = "7ZlAakEf0Qg", + ExportName = "pthread_attr_setinheritsched", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadAttrSetinheritschedPOSIX(CpuContext ctx) => PthreadAttrSetinheritsched(ctx); + + [SysAbiExport( + Nid = "0qOtCR-ZHck", + ExportName = "pthread_attr_getstacksize", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadAttrGetstacksizePOSIX(CpuContext ctx) => PthreadAttrGetstacksize(ctx); + + [SysAbiExport( + Nid = "VUT1ZSrHT0I", + ExportName = "pthread_attr_getdetachstate", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadAttrGetdetachstatePOSIX(CpuContext ctx) => PthreadAttrGetdetachstate(ctx); + + [SysAbiExport( + Nid = "JKyG3SWyA10", + ExportName = "pthread_attr_setguardsize", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadAttrSetguardsizePOSIX(CpuContext ctx) => PthreadAttrSetguardsize(ctx); + + [SysAbiExport( + Nid = "JNkVVsVDmOk", + ExportName = "pthread_attr_getguardsize", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadAttrGetguardsizePOSIX(CpuContext ctx) => PthreadAttrGetguardsize(ctx); } diff --git a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs index b63881f2..cd9a7ac2 100644 --- a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs @@ -2058,6 +2058,13 @@ public static class KernelRuntimeCompatExports LibraryName = "libKernel")] public static int KernelNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: false); + [SysAbiExport( + Nid = "NhpspxdjEKU", + ExportName = "_nanosleep", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixNanosleepUnderscore(CpuContext ctx) => NanosleepCore(ctx, posix: true); + [SysAbiExport( Nid = "yS8U2TGCe1A", ExportName = "nanosleep", diff --git a/src/SharpEmu.Libs/Network/NetExports.cs b/src/SharpEmu.Libs/Network/NetExports.cs index 17f75fad..0751a323 100644 --- a/src/SharpEmu.Libs/Network/NetExports.cs +++ b/src/SharpEmu.Libs/Network/NetExports.cs @@ -184,6 +184,212 @@ public static class NetExports return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); } + /// + /// POSIX alias of ; identical + /// (fd, level, option, value, length) argument order. + /// + [SysAbiExport( + Nid = "fFxGkxF2bVo", + ExportName = "setsockopt", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixSetsockopt(CpuContext ctx) => NetSetsockopt(ctx); + + /// + /// Reads back the socket options this backend actually tracks: SO_NBIO, + /// SO_REUSEADDR and SO_ERROR. + /// + /// + /// Anything else returns EINVAL rather than a zero-filled buffer. A caller + /// that receives success for an option nobody stored would treat whatever + /// happens to be in its output buffer as the real setting, which is a harder + /// failure to trace than an explicit rejection. + /// + [SysAbiExport( + Nid = "6O8EwYOgH9Y", + ExportName = "getsockopt", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixGetsockopt(CpuContext ctx) + { + var id = unchecked((int)ctx[CpuRegister.Rdi]); + var level = unchecked((int)ctx[CpuRegister.Rsi]); + var option = unchecked((int)ctx[CpuRegister.Rdx]); + var valueAddress = ctx[CpuRegister.Rcx]; + var lengthAddress = ctx[CpuRegister.R8]; + if (!_sockets.TryGetValue(id, out var socket)) + { + return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor); + } + + if (valueAddress == 0 || lengthAddress == 0 || level != 0xFFFF) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + Span lengthBytes = stackalloc byte[sizeof(int)]; + if (!ctx.Memory.TryRead(lengthAddress, lengthBytes)) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + if (BinaryPrimitives.ReadInt32LittleEndian(lengthBytes) < sizeof(int)) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + int value; + switch (option) + { + // ORBIS_NET_SO_NBIO: mirrors what sceNetSetsockopt stored. + case 0x1200: + value = socket.Blocking ? 0 : 1; + break; + case 0x0004: + value = (int)socket.GetSocketOption( + SocketOptionLevel.Socket, + SocketOptionName.ReuseAddress)! != 0 ? 1 : 0; + break; + // ORBIS_NET_SO_ERROR: nothing here records per-socket async errors, + // so report "no pending error" rather than inventing one. + case 0x1007: + value = 0; + break; + default: + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + Span valueBytes = stackalloc byte[sizeof(int)]; + BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value); + BinaryPrimitives.WriteInt32LittleEndian(lengthBytes, sizeof(int)); + if (!ctx.Memory.TryWrite(valueAddress, valueBytes) || + !ctx.Memory.TryWrite(lengthAddress, lengthBytes)) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + TraceNet("socket.getsockopt", id, unchecked((uint)option), unchecked((uint)value), 0); + return ctx.SetReturn(0); + } + + [SysAbiExport( + Nid = "fZOeZIOEmLw", + ExportName = "send", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixSend(CpuContext ctx) + { + var id = unchecked((int)ctx[CpuRegister.Rdi]); + var bufferAddress = ctx[CpuRegister.Rsi]; + var length = unchecked((int)ctx[CpuRegister.Rdx]); + if (!_sockets.TryGetValue(id, out var socket)) + { + return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor); + } + + if (length < 0 || (length != 0 && bufferAddress == 0)) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + if (length == 0) + { + return ctx.SetReturn(0); + } + + var payload = new byte[length]; + if (!ctx.Memory.TryRead(bufferAddress, payload)) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + try + { + var sent = socket.Send(payload, SocketFlags.None); + TraceNet("socket.send", id, unchecked((uint)length), unchecked((uint)sent), 0); + return ctx.SetReturn(sent); + } + catch (SocketException exception) + when (exception.SocketErrorCode == SocketError.WouldBlock) + { + return SetNetError(ctx, NetErrorWouldBlock, NetErrnoWouldBlock); + } + catch (SocketException) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + catch (ObjectDisposedException) + { + return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor); + } + } + + /// + /// Formats a binary address as text. Pure conversion with no socket state, + /// so it behaves identically to the console version for AF_INET/AF_INET6. + /// + [SysAbiExport( + Nid = "5jRCs2axtr4", + ExportName = "inet_ntop", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixInetNtop(CpuContext ctx) + { + var family = unchecked((int)ctx[CpuRegister.Rdi]); + var sourceAddress = ctx[CpuRegister.Rsi]; + var destinationAddress = ctx[CpuRegister.Rdx]; + var destinationSize = unchecked((int)ctx[CpuRegister.Rcx]); + if (sourceAddress == 0 || destinationAddress == 0 || destinationSize <= 0) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + // ORBIS_NET_AF_INET / ORBIS_NET_AF_INET6, matching TryMapAddressFamily. + var addressLength = family switch + { + 2 => 4, + 28 => 16, + _ => 0, + }; + + if (addressLength == 0) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + var rawAddress = new byte[addressLength]; + if (!ctx.Memory.TryRead(sourceAddress, rawAddress)) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + var text = new IPAddress(rawAddress).ToString(); + var encoded = Encoding.ASCII.GetBytes(text); + + // POSIX requires the terminator to fit as well; a truncated address string + // is worse than a reported failure because the caller cannot detect it. + if (encoded.Length + 1 > destinationSize) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + var buffer = new byte[encoded.Length + 1]; + encoded.CopyTo(buffer, 0); + if (!ctx.Memory.TryWrite(destinationAddress, buffer)) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + // inet_ntop returns the destination pointer on success. + ctx[CpuRegister.Rax] = destinationAddress; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "bErx49PgxyY", ExportName = "sceNetBind", diff --git a/src/SharpEmu.Libs/Np/NpManagerExports.cs b/src/SharpEmu.Libs/Np/NpManagerExports.cs index e5d44fe2..1247683b 100644 --- a/src/SharpEmu.Libs/Np/NpManagerExports.cs +++ b/src/SharpEmu.Libs/Np/NpManagerExports.cs @@ -69,6 +69,23 @@ public static class NpManagerExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + /// + /// Accepts the reachability callback and never invokes it. Reachability + /// transitions only ever fire on a real PSN connection, which an offline + /// session does not have, so registering successfully and staying silent is + /// the accurate emulation of a signed-out console rather than a stub. + /// + [SysAbiExport( + Nid = "hw5KNqAAels", + ExportName = "sceNpRegisterNpReachabilityStateCallback", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNpManager")] + public static int NpRegisterNpReachabilityStateCallback(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "qQJfO8HAiaY", ExportName = "sceNpRegisterStateCallbackA", diff --git a/src/SharpEmu.Libs/Np/NpTrophy2Exports.cs b/src/SharpEmu.Libs/Np/NpTrophy2Exports.cs index 4a978dbf..c636d62d 100644 --- a/src/SharpEmu.Libs/Np/NpTrophy2Exports.cs +++ b/src/SharpEmu.Libs/Np/NpTrophy2Exports.cs @@ -80,6 +80,25 @@ public static class NpTrophy2Exports LibraryName = "libSceNpTrophy2")] public static int NpTrophy2ShowTrophyList(CpuContext ctx) => ReturnOk(ctx); + /// + /// Gen5 ABI: context, handle, trophy id, then SceNpTrophy2Details and + /// SceNpTrophy2Data output pointers. + /// + /// + /// Reports "no such trophy" rather than succeeding. Succeeding would require + /// filling both output structures, and their exact layouts are not confirmed + /// here — a title that trusted zeroed details would read an empty name and a + /// grade of zero as real data. NOT_FOUND is a documented outcome that callers + /// must already handle, so it degrades along a path the game tests. + /// + [SysAbiExport( + Nid = "EwNylPdWUTM", + ExportName = "sceNpTrophy2GetTrophyInfo", + Target = Generation.Gen5, + LibraryName = "libSceNpTrophy2")] + public static int NpTrophy2GetTrophyInfo(CpuContext ctx) => + SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); + private static int WriteIdAndReturn(CpuContext ctx, ulong outAddress, ref int nextId) { if (outAddress == 0)