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)