Files
sharpemu/src/SharpEmu.Libs/Network/NetExports.cs
T
Miguel Cruz 864cbb0fa0 [AGC/Vulkan] Extend PS5 runtime and rendering compatibility (#216)
* [Core] Add POSIX native execution and PS5 SELF support

Extend the native backend, guest TLS, fixed-address memory, and loader paths needed by PS5 titles on Windows, Linux, and macOS. Keep workstation GC so high-core-count hosts do not reserve over fixed guest image bases.

* [HLE] Expand PS5 service and media compatibility

Add the kernel, threading, save-data, networking, audio, video-codec, font, dialog, and service exports required by newer PS5 software. Preserve every SysAbi NID currently registered by main while adding the compatibility surface used by ASTRO BOT.

* [AGC/Vulkan] Extend Gen5 shader and presentation support

Expand PM4 handling, Gen5 shader translation, MRT and packed export support, guest image tracking, depth initialization, texture aliasing, and Vulkan presentation. Add the performance overlay and address-filtered diagnostics used to validate ASTRO BOT with original shaders.

* [Core] Align static TLS reservation across hosts

* [Pad] Align primary user ID with UserService

* [Gpu] Preserve runtime scalar buffers across renderer seam

* [AGC] Restore omitted command helper exports

* [Vulkan] Reuse primary views for promoted MRT targets

* [Vulkan] Preserve scratch storage bindings in compute dispatches
2026-07-16 02:02:34 +03:00

555 lines
19 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Network;
public static class NetExports
{
private const int NetErrorBadFileDescriptor = unchecked((int)0x80410109);
private const int NetErrorInvalidArgument = unchecked((int)0x80410116);
private const int NetErrorWouldBlock = unchecked((int)0x80410123);
private const int NetErrorAddressInUse = unchecked((int)0x80410130);
private const int NetErrorNotInitialized = unchecked((int)0x804101C8);
private const int NetErrnoBadFileDescriptor = 9;
private const int NetErrnoInvalidArgument = 22;
private const int NetErrnoWouldBlock = 35;
private const int NetErrnoAddressInUse = 48;
private const int NetErrnoNotInitialized = 200;
private const int MaxNameLength = 256;
private static readonly ConcurrentDictionary<int, NetPool> _pools = new();
private static readonly ConcurrentDictionary<int, ResolverContext> _resolvers = new();
private static readonly ConcurrentDictionary<int, Socket> _sockets = new();
private static int _nextPoolId;
private static int _nextResolverId = 0x2000;
private static int _nextSocketId = 0x4000;
// The platform networking module is usable immediately after it is loaded.
// Games and middleware (notably FMOD) can create internal sockets before an
// explicit sceNetInit call reaches application code.
private static bool _initialized = true;
[ThreadStatic]
private static nint _errnoAddress;
private sealed record NetPool(string Name, int Size, int Flags);
private sealed record ResolverContext(string Name, int PoolId, int Flags, int LastError);
[SysAbiExport(
Nid = "Nlev7Lg8k3A",
ExportName = "sceNetInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetInit(CpuContext ctx)
{
_initialized = true;
TraceNet("init", 0, 0, 0, 0);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "cTGkc6-TBlI",
ExportName = "sceNetTerm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetTerm(CpuContext ctx)
{
_initialized = false;
_pools.Clear();
_resolvers.Clear();
foreach (var socket in _sockets.Values)
{
socket.Dispose();
}
_sockets.Clear();
TraceNet("term", 0, 0, 0, 0);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "Q4qBuN-c0ZM",
ExportName = "sceNetSocket",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetSocket(CpuContext ctx)
{
if (!_initialized)
{
return SetNetError(ctx, NetErrorNotInitialized, NetErrnoNotInitialized);
}
var nameAddress = ctx[CpuRegister.Rdi];
var family = unchecked((int)ctx[CpuRegister.Rsi]);
var type = unchecked((int)ctx[CpuRegister.Rdx]);
var protocol = unchecked((int)ctx[CpuRegister.Rcx]);
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
? value
: string.Empty;
if (!TryTranslateSocketParameters(family, type, protocol, out var addressFamily, out var socketType, out var protocolType))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
try
{
var socket = new Socket(addressFamily, socketType, protocolType);
var id = Interlocked.Increment(ref _nextSocketId);
_sockets[id] = socket;
TraceNet("socket.create", id, unchecked((ulong)family), unchecked((ulong)type), unchecked((ulong)protocol));
ctx[CpuRegister.Rax] = unchecked((ulong)id);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (SocketException)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
}
[SysAbiExport(
Nid = "45ggEzakPJQ",
ExportName = "sceNetSocketClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetSocketClose(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_sockets.TryRemove(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
socket.Dispose();
TraceNet("socket.close", id, 0, 0, 0);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "2mKX2Spso7I",
ExportName = "sceNetSetsockopt",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetSetsockopt(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 valueLength = unchecked((int)ctx[CpuRegister.R8]);
if (!_sockets.TryGetValue(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
// ORBIS_NET_SOL_SOCKET / ORBIS_NET_SO_NBIO. This is the first option
// used by FMOD's discovery socket and maps directly to host blocking.
if (level == 0xFFFF && option == 0x1200)
{
Span<byte> value = stackalloc byte[sizeof(int)];
if (valueLength < value.Length || valueAddress == 0 || !ctx.Memory.TryRead(valueAddress, value))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
socket.Blocking = BinaryPrimitives.ReadInt32LittleEndian(value) == 0;
TraceNet("socket.nonblocking", id, socket.Blocking ? 0UL : 1UL, 0, 0);
return ctx.SetReturn(0);
}
// ORBIS_NET_SO_REUSEADDR uses the BSD value 0x0004.
if (level == 0xFFFF && option == 0x0004)
{
Span<byte> value = stackalloc byte[sizeof(int)];
if (valueLength < value.Length || valueAddress == 0 || !ctx.Memory.TryRead(valueAddress, value))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
socket.SetSocketOption(
SocketOptionLevel.Socket,
SocketOptionName.ReuseAddress,
BinaryPrimitives.ReadInt32LittleEndian(value) != 0);
TraceNet("socket.reuseaddr", id, BinaryPrimitives.ReadUInt32LittleEndian(value), 0, 0);
return ctx.SetReturn(0);
}
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
[SysAbiExport(
Nid = "bErx49PgxyY",
ExportName = "sceNetBind",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetBind(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_sockets.TryGetValue(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
if (!TryReadSocketAddress(ctx, ctx[CpuRegister.Rsi], unchecked((int)ctx[CpuRegister.Rdx]), out var endpoint))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
try
{
socket.Bind(endpoint);
TraceNet("socket.bind", id, unchecked((ulong)endpoint.Port), 0, 0);
return ctx.SetReturn(0);
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
{
return SetNetError(ctx, NetErrorAddressInUse, NetErrnoAddressInUse);
}
catch (SocketException)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
}
[SysAbiExport(
Nid = "kOj1HiAGE54",
ExportName = "sceNetListen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetListen(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_sockets.TryGetValue(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
try
{
socket.Listen(Math.Max(0, unchecked((int)ctx[CpuRegister.Rsi])));
TraceNet("socket.listen", id, ctx[CpuRegister.Rsi], 0, 0);
return ctx.SetReturn(0);
}
catch (SocketException)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
}
[SysAbiExport(
Nid = "PIWqhn9oSxc",
ExportName = "sceNetAccept",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetAccept(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_sockets.TryGetValue(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
try
{
var accepted = socket.Accept();
var acceptedId = Interlocked.Increment(ref _nextSocketId);
_sockets[acceptedId] = accepted;
TraceNet("socket.accept", acceptedId, unchecked((ulong)id), 0, 0);
ctx[CpuRegister.Rax] = unchecked((ulong)acceptedId);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (SocketException ex) when (ex.SocketErrorCode is SocketError.WouldBlock or SocketError.IOPending)
{
return SetNetError(ctx, NetErrorWouldBlock, NetErrnoWouldBlock);
}
catch (SocketException)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
}
[SysAbiExport(
Nid = "HQOwnfMGipQ",
ExportName = "sceNetErrnoLoc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetErrnoLoc(CpuContext ctx)
{
if (_errnoAddress == 0)
{
_errnoAddress = Marshal.AllocHGlobal(sizeof(int));
Marshal.WriteInt32(_errnoAddress, 0);
}
ctx[CpuRegister.Rax] = unchecked((ulong)_errnoAddress);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "dgJBaeJnGpo",
ExportName = "sceNetPoolCreate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetPoolCreate(CpuContext ctx)
{
var nameAddress = ctx[CpuRegister.Rdi];
var size = unchecked((int)ctx[CpuRegister.Rsi]);
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
if (size <= 0)
{
return ctx.SetReturn(NetErrorInvalidArgument);
}
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
? value
: string.Empty;
var id = Interlocked.Increment(ref _nextPoolId);
_pools[id] = new NetPool(name, size, flags);
TraceNet("pool.create", id, unchecked((ulong)size), unchecked((ulong)flags), _initialized ? 1UL : 0UL);
ctx[CpuRegister.Rax] = unchecked((ulong)id);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "K7RlrTkI-mw",
ExportName = "sceNetPoolDestroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetPoolDestroy(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_pools.TryRemove(id, out _))
{
return ctx.SetReturn(NetErrorBadFileDescriptor);
}
TraceNet("pool.destroy", id, 0, 0, _initialized ? 1UL : 0UL);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "9T2pDF2Ryqg",
ExportName = "sceNetHtonl",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetHtonl(CpuContext ctx)
{
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "iWQWrwiSt8A",
ExportName = "sceNetHtons",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetHtons(CpuContext ctx)
{
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "pQGpHYopAIY",
ExportName = "sceNetNtohl",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetNtohl(CpuContext ctx)
{
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "Rbvt+5Y2iEw",
ExportName = "sceNetNtohs",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetNtohs(CpuContext ctx)
{
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "C4UgDHHPvdw",
ExportName = "sceNetResolverCreate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetResolverCreate(CpuContext ctx)
{
var nameAddress = ctx[CpuRegister.Rdi];
var poolId = unchecked((int)ctx[CpuRegister.Rsi]);
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
if (flags != 0)
{
return ctx.SetReturn(NetErrorInvalidArgument);
}
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
? value
: string.Empty;
var id = Interlocked.Increment(ref _nextResolverId);
_resolvers[id] = new ResolverContext(name, poolId, flags, 0);
TraceNet("resolver.create", id, unchecked((ulong)poolId), unchecked((ulong)flags), _initialized ? 1UL : 0UL);
ctx[CpuRegister.Rax] = unchecked((ulong)id);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "kJlYH5uMAWI",
ExportName = "sceNetResolverDestroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetResolverDestroy(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
return _resolvers.TryRemove(id, out _)
? ctx.SetReturn(0)
: ctx.SetReturn(NetErrorBadFileDescriptor);
}
[SysAbiExport(
Nid = "J5i3hiLJMPk",
ExportName = "sceNetResolverGetError",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetResolverGetError(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
var statusAddress = ctx[CpuRegister.Rsi];
if (statusAddress == 0)
{
return ctx.SetReturn(NetErrorInvalidArgument);
}
if (!_resolvers.TryGetValue(id, out var resolver))
{
return ctx.SetReturn(NetErrorBadFileDescriptor);
}
Span<byte> status = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(status, resolver.LastError);
return ctx.Memory.TryWrite(statusAddress, status)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int SetNetError(CpuContext ctx, int result, int errno)
{
if (_errnoAddress == 0)
{
_errnoAddress = Marshal.AllocHGlobal(sizeof(int));
}
Marshal.WriteInt32(_errnoAddress, errno);
return ctx.SetReturn(result);
}
private static bool TryTranslateSocketParameters(
int family,
int type,
int protocol,
out AddressFamily addressFamily,
out SocketType socketType,
out ProtocolType protocolType)
{
addressFamily = family switch
{
2 => AddressFamily.InterNetwork,
28 => AddressFamily.InterNetworkV6,
_ => AddressFamily.Unspecified,
};
socketType = type switch
{
1 => SocketType.Stream,
2 => SocketType.Dgram,
_ => SocketType.Unknown,
};
protocolType = protocol switch
{
0 when socketType == SocketType.Stream => ProtocolType.Tcp,
0 when socketType == SocketType.Dgram => ProtocolType.Udp,
6 => ProtocolType.Tcp,
17 => ProtocolType.Udp,
_ => ProtocolType.Unknown,
};
return addressFamily != AddressFamily.Unspecified &&
socketType != SocketType.Unknown &&
protocolType != ProtocolType.Unknown;
}
private static bool TryReadSocketAddress(CpuContext ctx, ulong address, int length, out IPEndPoint endpoint)
{
endpoint = new IPEndPoint(IPAddress.Any, 0);
if (address == 0 || length < 16)
{
return false;
}
Span<byte> bytes = stackalloc byte[16];
if (!ctx.Memory.TryRead(address, bytes) || bytes[1] != 2)
{
return false;
}
var port = BinaryPrimitives.ReadUInt16BigEndian(bytes[2..4]);
endpoint = new IPEndPoint(new IPAddress(bytes[4..8]), port);
return true;
}
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
{
value = string.Empty;
if (address == 0)
{
return true;
}
Span<byte> one = stackalloc byte[1];
var bytes = new byte[maxLength];
var count = 0;
for (; count < maxLength; count++)
{
if (!ctx.Memory.TryRead(address + (ulong)count, one))
{
return false;
}
if (one[0] == 0)
{
break;
}
bytes[count] = one[0];
}
value = Encoding.UTF8.GetString(bytes, 0, count);
return true;
}
private static void TraceNet(string operation, int id, ulong arg0, ulong arg1, ulong arg2)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] net.{operation} id={id} arg0=0x{arg0:X16} arg1=0x{arg1:X16} arg2=0x{arg2:X16}");
}
}