From 53f233da9b127ea3d46f7a9e6daa29828f0791cb Mon Sep 17 00:00:00 2001 From: ParantezTech <12572227+par274@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:28:05 +0300 Subject: [PATCH] [Network] Add socket epoll registration and readiness waits Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com> --- src/SharpEmu.Libs/Network/NetExports.cs | 357 ++++++++++++++++++++++++ 1 file changed, 357 insertions(+) diff --git a/src/SharpEmu.Libs/Network/NetExports.cs b/src/SharpEmu.Libs/Network/NetExports.cs index ed2c0dc..91c6aad 100644 --- a/src/SharpEmu.Libs/Network/NetExports.cs +++ b/src/SharpEmu.Libs/Network/NetExports.cs @@ -14,28 +14,42 @@ namespace SharpEmu.Libs.Network; public static class NetExports { + private const int NetErrorNoEntry = unchecked((int)0x80410102); private const int NetErrorBadFileDescriptor = unchecked((int)0x80410109); private const int NetErrorFault = unchecked((int)0x8041010E); + private const int NetErrorAlreadyExists = unchecked((int)0x80410111); private const int NetErrorInvalidArgument = unchecked((int)0x80410116); private const int NetErrorTooManyFiles = unchecked((int)0x80410118); private const int NetErrorWouldBlock = unchecked((int)0x80410123); private const int NetErrorAddressInUse = unchecked((int)0x80410130); + private const int NetErrorTooManyOpenFiles = unchecked((int)0x80410118); private const int NetErrorResolverNoDns = unchecked((int)0x804101E1); private const int NetErrorNotInitialized = unchecked((int)0x804101C8); + private const int NetErrnoNoEntry = 2; private const int NetErrnoBadFileDescriptor = 9; private const int NetErrnoFault = 14; + private const int NetErrnoAlreadyExists = 17; private const int NetErrnoInvalidArgument = 22; private const int NetErrnoTooManyFiles = 24; private const int NetErrnoWouldBlock = 35; private const int NetErrnoAddressInUse = 48; + private const int NetErrnoTooManyOpenFiles = 24; private const int NetErrnoNotInitialized = 200; private const int NetErrnoResolverNoDns = 225; private const int MaxNameLength = 256; + private const int EpollIdBase = 1024; + private const int EpollCapacity = 32; + private const int NetEpollEventSize = 24; + private const uint NetEpollIn = 0x00000001; + private const uint NetEpollOut = 0x00000002; + private const uint NetEpollError = 0x00000008; private static ReadOnlySpan OfflineMacAddress => [0x02, 0x53, 0x48, 0x41, 0x52, 0x50]; private static readonly ConcurrentDictionary _pools = new(); private static readonly ConcurrentDictionary _resolvers = new(); private static readonly ConcurrentDictionary _sockets = new(); + private static readonly object EpollGate = new(); + private static readonly Dictionary _epolls = new(); private static int _nextPoolId; private static int _nextResolverId = 0x2000; private static int _nextSocketId = 255; @@ -52,6 +66,26 @@ public static class NetExports private sealed record ResolverContext(string Name, int PoolId, int Flags, int LastError); + private sealed class NetEpollContext(string name) + { + public string Name { get; } = name; + + public List Registrations { get; } = []; + } + + private readonly record struct NetEpollRegistration(int SocketId, NetEpollEvent Event); + + private readonly record struct NetEpollEvent(uint Events, ulong Data); + + internal static void ResetEpollsForTests() + { + lock (EpollGate) + { + _epolls.Clear(); + Monitor.PulseAll(EpollGate); + } + } + internal static bool TryGetReadEventState( int socketId, ulong lowWater, @@ -116,6 +150,11 @@ public static class NetExports _initialized = false; _pools.Clear(); _resolvers.Clear(); + lock (EpollGate) + { + _epolls.Clear(); + Monitor.PulseAll(EpollGate); + } foreach (var socket in _sockets.Values) { socket.Dispose(); @@ -721,6 +760,291 @@ public static class NetExports Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceNet")] public static int NetPoolCreate(CpuContext ctx) + [SysAbiExport( + Nid = "SF47kB2MNTo", + ExportName = "sceNetEpollCreate", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNet")] + public static int NetEpollCreate(CpuContext ctx) + { + if (!_initialized) + { + return SetNetError(ctx, NetErrorNotInitialized, NetErrnoNotInitialized); + } + + var nameAddress = ctx[CpuRegister.Rdi]; + var flags = unchecked((int)ctx[CpuRegister.Rsi]); + if (nameAddress == 0 || flags != 0 || + !TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var name)) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + lock (EpollGate) + { + for (var index = 0; index < EpollCapacity; index++) + { + var id = EpollIdBase + index; + if (_epolls.ContainsKey(id)) + { + continue; + } + + _epolls.Add(id, new NetEpollContext(name)); + TraceNet("epoll.create", id, nameAddress, 0, 0); + return ctx.SetReturn(id); + } + } + + return SetNetError(ctx, NetErrorTooManyOpenFiles, NetErrnoTooManyOpenFiles); + } + + [SysAbiExport( + Nid = "ZVw46bsasAk", + ExportName = "sceNetEpollControl", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNet")] + public static int NetEpollControl(CpuContext ctx) + { + const int EpollControlAdd = 1; + const int EpollControlModify = 2; + const int EpollControlDelete = 3; + + var epollId = unchecked((int)ctx[CpuRegister.Rdi]); + var operation = unchecked((int)ctx[CpuRegister.Rsi]); + var socketId = unchecked((int)ctx[CpuRegister.Rdx]); + var eventAddress = ctx[CpuRegister.Rcx]; + + if (operation is < EpollControlAdd or > EpollControlDelete || + (operation is EpollControlAdd or EpollControlModify && eventAddress == 0) || + (operation == EpollControlDelete && eventAddress != 0)) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + if (!_sockets.ContainsKey(socketId)) + { + return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor); + } + + NetEpollEvent requestedEvent = default; + if (eventAddress != 0 && !TryReadNetEpollEvent(ctx, eventAddress, out requestedEvent)) + { + return SetNetError(ctx, NetErrorFault, NetErrnoFault); + } + + lock (EpollGate) + { + if (!_epolls.TryGetValue(epollId, out var epoll)) + { + return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor); + } + + var registrationIndex = epoll.Registrations.FindIndex(value => value.SocketId == socketId); + switch (operation) + { + case EpollControlAdd when registrationIndex >= 0: + return SetNetError(ctx, NetErrorAlreadyExists, NetErrnoAlreadyExists); + case EpollControlAdd: + epoll.Registrations.Add(new NetEpollRegistration(socketId, requestedEvent)); + break; + case EpollControlModify when registrationIndex < 0: + return SetNetError(ctx, NetErrorNoEntry, NetErrnoNoEntry); + case EpollControlModify: + epoll.Registrations[registrationIndex] = + new NetEpollRegistration(socketId, requestedEvent); + break; + case EpollControlDelete when registrationIndex < 0: + return SetNetError(ctx, NetErrorNoEntry, NetErrnoNoEntry); + case EpollControlDelete: + epoll.Registrations.RemoveAt(registrationIndex); + break; + } + + Monitor.PulseAll(EpollGate); + } + + TraceNet( + "epoll.control", + epollId, + unchecked((uint)operation), + unchecked((uint)socketId), + requestedEvent.Events); + return ctx.SetReturn(0); + } + + [SysAbiExport( + Nid = "drjIbDbA7UQ", + ExportName = "sceNetEpollWait", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNet")] + public static int NetEpollWait(CpuContext ctx) + { + var epollId = unchecked((int)ctx[CpuRegister.Rdi]); + var eventsAddress = ctx[CpuRegister.Rsi]; + var maxEvents = unchecked((int)ctx[CpuRegister.Rdx]); + var timeoutMicroseconds = unchecked((int)ctx[CpuRegister.Rcx]); + + if (eventsAddress == 0) + { + return SetNetError(ctx, NetErrorFault, NetErrnoFault); + } + + if (maxEvents <= 0 || timeoutMicroseconds < -1) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + NetEpollRegistration[] registrations; + lock (EpollGate) + { + if (!_epolls.TryGetValue(epollId, out var epoll)) + { + return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor); + } + + if (epoll.Registrations.Count == 0 && timeoutMicroseconds != 0) + { + if (timeoutMicroseconds < 0) + { + while (_epolls.TryGetValue(epollId, out epoll) && epoll.Registrations.Count == 0) + { + Monitor.Wait(EpollGate); + } + } + else + { + Monitor.Wait( + EpollGate, + TimeSpan.FromTicks((long)timeoutMicroseconds * TimeSpan.TicksPerMicrosecond)); + } + + if (!_epolls.TryGetValue(epollId, out epoll)) + { + return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor); + } + } + + registrations = epoll.Registrations.ToArray(); + } + + if (registrations.Length == 0) + { + TraceNet("epoll.wait", epollId, unchecked((uint)maxEvents), unchecked((uint)timeoutMicroseconds), 0); + return ctx.SetReturn(0); + } + + var readSockets = new List(); + var writeSockets = new List(); + var errorSockets = new List(); + var hostRegistrations = new Dictionary(); + foreach (var registration in registrations) + { + if (!_sockets.TryGetValue(registration.SocketId, out var socket)) + { + continue; + } + + hostRegistrations[socket] = registration; + if ((registration.Event.Events & NetEpollIn) != 0) + { + readSockets.Add(socket); + } + if ((registration.Event.Events & NetEpollOut) != 0) + { + writeSockets.Add(socket); + } + errorSockets.Add(socket); + } + + if (hostRegistrations.Count == 0) + { + return ctx.SetReturn(0); + } + + try + { + Socket.Select(readSockets, writeSockets, errorSockets, timeoutMicroseconds); + } + catch (ObjectDisposedException) + { + return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor); + } + catch (SocketException) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + var readyCount = 0; + foreach (var (socket, registration) in hostRegistrations) + { + uint readyEvents = 0; + if (readSockets.Contains(socket)) + { + readyEvents |= NetEpollIn; + } + if (writeSockets.Contains(socket)) + { + readyEvents |= NetEpollOut; + } + if (errorSockets.Contains(socket)) + { + readyEvents |= NetEpollError; + } + if (readyEvents == 0) + { + continue; + } + + var outputAddress = eventsAddress + (ulong)(readyCount * NetEpollEventSize); + if (!TryWriteNetEpollEvent( + ctx, + outputAddress, + readyEvents, + registration.SocketId, + registration.Event.Data)) + { + return SetNetError(ctx, NetErrorFault, NetErrnoFault); + } + + readyCount++; + if (readyCount == maxEvents) + { + break; + } + } + + TraceNet( + "epoll.wait", + epollId, + unchecked((uint)maxEvents), + unchecked((uint)timeoutMicroseconds), + unchecked((uint)readyCount)); + return ctx.SetReturn(readyCount); + } + + [SysAbiExport( + Nid = "Inp1lfL+Jdw", + ExportName = "sceNetEpollDestroy", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNet")] + public static int NetEpollDestroy(CpuContext ctx) + { + var epollId = unchecked((int)ctx[CpuRegister.Rdi]); + lock (EpollGate) + { + if (!_epolls.Remove(epollId)) + { + return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor); + } + + Monitor.PulseAll(EpollGate); + } + + TraceNet("epoll.destroy", epollId, 0, 0, 0); + return ctx.SetReturn(0); + } + { var nameAddress = ctx[CpuRegister.Rdi]; var size = unchecked((int)ctx[CpuRegister.Rsi]); @@ -1078,6 +1402,39 @@ public static class NetExports LibraryName = "libSceNet")] public static int NetInetNtop(CpuContext ctx) => PosixInetNtop(ctx); + private static bool TryReadNetEpollEvent( + CpuContext ctx, + ulong address, + out NetEpollEvent epollEvent) + { + Span bytes = stackalloc byte[NetEpollEventSize]; + if (!ctx.Memory.TryRead(address, bytes)) + { + epollEvent = default; + return false; + } + + epollEvent = new NetEpollEvent( + BinaryPrimitives.ReadUInt32LittleEndian(bytes), + BinaryPrimitives.ReadUInt64LittleEndian(bytes[16..])); + return true; + } + + private static bool TryWriteNetEpollEvent( + CpuContext ctx, + ulong address, + uint events, + int socketId, + ulong data) + { + Span bytes = stackalloc byte[NetEpollEventSize]; + bytes.Clear(); + BinaryPrimitives.WriteUInt32LittleEndian(bytes, events); + BinaryPrimitives.WriteUInt64LittleEndian(bytes[8..], unchecked((uint)socketId)); + BinaryPrimitives.WriteUInt64LittleEndian(bytes[16..], data); + return ctx.Memory.TryWrite(address, bytes); + } + 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))