diff --git a/src/SharpEmu.Libs/Network/RudpExports.cs b/src/SharpEmu.Libs/Network/RudpExports.cs new file mode 100644 index 0000000..34df752 --- /dev/null +++ b/src/SharpEmu.Libs/Network/RudpExports.cs @@ -0,0 +1,121 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; + +namespace SharpEmu.Libs.Network; + +/// +/// Minimal reliable-UDP poll object support. SharpEmu does not expose a host +/// RUDP transport yet, but callers still require a distinct non-zero poll ID +/// for their offline/event-processing path. +/// +public static class RudpExports +{ + private const int MaxLivePolls = 4096; + private const ulong PollEventSize = 8; + private const int RudpErrorInvalidArgument = unchecked((int)0x80770004); + private const int RudpErrorInvalidPollId = unchecked((int)0x8077001F); + private const int RudpErrorOutOfMemory = unchecked((int)0x80770003); + private static readonly object PollGate = new(); + private static readonly Dictionary PollCapacities = []; + private static int _nextPollId; + + [SysAbiExport( + Nid = "MVbmLASjn5M", + ExportName = "sceRudpPollCreate", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceRudp")] + public static int RudpPollCreate(CpuContext ctx) + { + var requestedEvents = ctx[CpuRegister.Rdi]; + if (requestedEvents is 0 or > uint.MaxValue) + { + return ctx.SetReturn(RudpErrorInvalidArgument); + } + + lock (PollGate) + { + if (PollCapacities.Count >= MaxLivePolls) + { + return ctx.SetReturn(RudpErrorOutOfMemory); + } + + int pollId; + do + { + _nextPollId = _nextPollId == int.MaxValue ? 1 : _nextPollId + 1; + pollId = _nextPollId; + } + while (PollCapacities.ContainsKey(pollId)); + + PollCapacities.Add(pollId, unchecked((uint)requestedEvents)); + return ctx.SetReturn(pollId); + } + } + + [SysAbiExport( + Nid = "M6ggviwXpLs", + ExportName = "sceRudpPollWait", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceRudp")] + public static int RudpPollWait(CpuContext ctx) + { + var pollId = unchecked((int)ctx[CpuRegister.Rdi]); + var events = ctx[CpuRegister.Rsi]; + var maxEvents = ctx[CpuRegister.Rdx]; + var timeoutMicroseconds = ctx[CpuRegister.Rcx]; + + if (!TryGetPollCapacity(pollId, out _)) + { + return ctx.SetReturn(RudpErrorInvalidPollId); + } + + if (events == 0 || + maxEvents == 0 || + maxEvents > uint.MaxValue || + maxEvents > ulong.MaxValue / PollEventSize || + !ctx.Memory.CanRead(events, maxEvents * PollEventSize)) + { + return ctx.SetReturn(RudpErrorInvalidArgument); + } + + ctx[CpuRegister.Rax] = 0; + if (timeoutMicroseconds == 0 || + !GuestThreadExecution.IsGuestThread || + !GuestThreadExecution.TryGetCurrentImportCallFrame(out _)) + { + return 0; + } + + var timeout = timeoutMicroseconds > (ulong)(TimeSpan.MaxValue.Ticks / 10) + ? TimeSpan.MaxValue + : TimeSpan.FromTicks(checked((long)timeoutMicroseconds * 10)); + _ = GuestThreadExecution.RequestCurrentThreadBlock( + ctx, + "sceRudpPollWait", + $"rudp-poll:{pollId}", + waiter: null, + blockDeadlineTimestamp: GuestThreadExecution.ComputeDeadlineTimestamp(timeout)); + return 0; + } + + public static void ResetRuntimeState() + { + lock (PollGate) + { + PollCapacities.Clear(); + _nextPollId = 0; + } + } + + internal static bool TryGetPollCapacity(int pollId, out uint capacity) + { + lock (PollGate) + { + return PollCapacities.TryGetValue(pollId, out capacity); + } + } + + internal static void ResetForTests() => ResetRuntimeState(); +}