From 759c9e590afe80d21b923cb57cb3fa2da2b7f893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 28 Oct 2026 09:10:42 +0100 Subject: [PATCH 1/2] sceNetInet: Poll and select as on hardware, and host sockets past FD_SETSIZE Following pspautotests net/inet/polledge: - poll reports only what was asked for, and never POLLHUP or POLLERR, even for a peer that closed or reset the connection. - select with a closed descriptor in a set fails with EBADF and leaves the sets alone. It used to skip it. The host side now uses poll() outside Windows, where select() can't take a descriptor numbered FD_SETSIZE or above and FD_SET wrote past the fd_set. macOS reports a peer that closed as POLLHUP with POLLPRI, and the POLLPRI is ignored, as its select() shows nothing for it. Winsock's select() has no such limit, but takes only 64 sockets a set, so it's called on groups of 64, and poll is no longer cut short at 64 entries there. A failing poll sets the host's errno rather than EINTR. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceNetInet.cpp | 239 ++++++++++++++++++++++------------------ test.py | 1 + 2 files changed, 132 insertions(+), 108 deletions(-) diff --git a/Core/HLE/sceNetInet.cpp b/Core/HLE/sceNetInet.cpp index 6d822f5167..1fe11d80b2 100644 --- a/Core/HLE/sceNetInet.cpp +++ b/Core/HLE/sceNetInet.cpp @@ -29,6 +29,10 @@ #ifdef __MINGW32__ #include #endif +#ifndef _WIN32 +#include +#endif +#include // This is the PSP networking errno. // It has a separate value per-thread, hence the map. @@ -521,75 +525,112 @@ static void ZeroInetFdSets(u32 readfdsPtr, u32 writefdsPtr, u32 exceptfdsPtr) { } } +// A host socket to check without waiting: in, what to check for, and out, what it's ready for. +struct HostSocketReady { + SOCKET sock; + bool read; + bool write; + bool except; +}; + +// Returns how many of the checks came back ready, like select(), or -1 with socket_errno set. +// Elsewhere select() can't take a descriptor numbered FD_SETSIZE or above, so this uses poll(), mapped +// as select() would see it. A hung-up socket counts as writable too: macOS reports a refused connect +// as just POLLHUP, which its select() shows as writable. Winsock's select() has no such limit, just one of FD_SETSIZE (64) +// sockets per set, so it's called on groups of that many. +static int CheckHostSockets(HostSocketReady *checks, int count) { + int ready = 0; +#if defined(_WIN32) + for (int start = 0; start < count; start += FD_SETSIZE) { + const int n = std::min(count - start, (int)FD_SETSIZE); + fd_set rd, wr, ex; + FD_ZERO(&rd); + FD_ZERO(&wr); + FD_ZERO(&ex); + bool anyRd = false, anyWr = false, anyEx = false; + for (int i = start; i < start + n; i++) { + if (checks[i].read) { FD_SET(checks[i].sock, &rd); anyRd = true; } + if (checks[i].write) { FD_SET(checks[i].sock, &wr); anyWr = true; } + if (checks[i].except) { FD_SET(checks[i].sock, &ex); anyEx = true; } + } + if (!anyRd && !anyWr && !anyEx) { + // Winsock's select() fails on empty sets. + continue; + } + timeval tmout = { 0, 0 }; + if (select(0, anyRd ? &rd : nullptr, anyWr ? &wr : nullptr, anyEx ? &ex : nullptr, &tmout) < 0) { + return -1; + } + for (int i = start; i < start + n; i++) { + checks[i].read = checks[i].read && FD_ISSET(checks[i].sock, &rd); + checks[i].write = checks[i].write && FD_ISSET(checks[i].sock, &wr); + checks[i].except = checks[i].except && FD_ISSET(checks[i].sock, &ex); + ready += checks[i].read + checks[i].write + checks[i].except; + } + } +#else + std::vector fds(count); + for (int i = 0; i < count; i++) { + fds[i].fd = checks[i].sock; + fds[i].events = (checks[i].read ? POLLIN : 0) | (checks[i].write ? POLLOUT : 0) | (checks[i].except ? POLLPRI : 0); + } + if (count > 0 && poll(fds.data(), count, 0) < 0) { + return -1; + } + for (int i = 0; i < count; i++) { + const short revents = fds[i].revents; + if (revents & POLLNVAL) { + errno = EBADF; + return -1; + } + checks[i].read = checks[i].read && (revents & (POLLIN | POLLHUP | POLLERR)); + checks[i].write = checks[i].write && (revents & (POLLOUT | POLLERR | POLLHUP)); + // macOS adds POLLPRI to POLLHUP when the peer closes, with nothing for select() to report. + checks[i].except = checks[i].except && (revents & POLLPRI) && !(revents & POLLHUP); + ready += checks[i].read + checks[i].write + checks[i].except; + } +#endif + return ready; +} + // args: nfds, readfds, writefds, exceptfds. The sets are only overwritten with the result when // something is ready, so they still hold the request between attempts. static InetTry TrySelect(SceUID threadID, const u32 *args, bool firstTry) { const int nfds = (int)args[0]; - SceNetInetFdSet *readfds = args[1] ? (SceNetInetFdSet*)Memory::GetPointerWriteOrException(args[1]) : nullptr; - SceNetInetFdSet *writefds = args[2] ? (SceNetInetFdSet*)Memory::GetPointerWriteOrException(args[2]) : nullptr; - SceNetInetFdSet *exceptfds = args[3] ? (SceNetInetFdSet*)Memory::GetPointerWriteOrException(args[3]) : nullptr; - - // First, translate the specified fd_sets to host sockets. - - fd_set rdfds, wrfds, exfds; - FD_ZERO(&rdfds); - FD_ZERO(&wrfds); - FD_ZERO(&exfds); - - int rdcnt = 0, wrcnt = 0, excnt = 0; - - int maxHostSocket = 0; - - // Save the mapping during setup. - SOCKET hostSockets[256]{}; - - struct { - SceNetInetFdSet *set; - fd_set *hostSet; - int *count; - const char *name; - } sets[] = { - { readfds, &rdfds, &rdcnt, "read" }, - { writefds, &wrfds, &wrcnt, "write" }, - { exceptfds, &exfds, &excnt, "except" }, + SceNetInetFdSet *sets[3] = { + args[1] ? (SceNetInetFdSet *)Memory::GetPointerWriteOrException(args[1]) : nullptr, + args[2] ? (SceNetInetFdSet *)Memory::GetPointerWriteOrException(args[2]) : nullptr, + args[3] ? (SceNetInetFdSet *)Memory::GetPointerWriteOrException(args[3]) : nullptr, }; + HostSocketReady checks[PSP_NET_INET_FD_SETSIZE]; + int guestFds[PSP_NET_INET_FD_SETSIZE]; + int count = 0; for (int i = SocketManager::MIN_VALID_INET_SOCKET; i < nfds; i++) { - for (auto &set : sets) { - if (!set.set || !NetInetFD_ISSET(i, set.set)) { - continue; - } - SOCKET sock; - if (!GetHostSocket(i, &sock)) { - if (firstTry) { - ERROR_LOG(Log::sceNet, "Bad socket #%d in %s fds", i, set.name); - } - continue; - } - hostSockets[i] = sock; - if ((int)sock > maxHostSocket) - maxHostSocket = (int)sock; - if (*set.count < FD_SETSIZE) { - FD_SET(sock, set.hostSet); // This might pointed to a non-existing socket or sockets belonged to other programs on Windows, because most of the time Windows socket have an id above 1k instead of 0-255 - (*set.count)++; - } else if (firstTry) { - ERROR_LOG(Log::sceNet, "Hit set size (%s)", set.name); - } + bool want[3]; + for (int s = 0; s < 3; s++) { + want[s] = sets[s] && NetInetFD_ISSET(i, sets[s]); } + if (!want[0] && !want[1] && !want[2]) { + continue; + } + SOCKET sock; + if (!GetHostSocket(i, &sock)) { + // Fails, leaving the sets alone (pspautotests net/inet/polledge). + UpdateErrnoFromHost(threadID, EBADF, "sceNetInetSelect"); + return { -1, false, 0 }; + } + checks[count] = { sock, want[0], want[1], want[2] }; + guestFds[count] = i; + count++; } - // Unlikely to hit these. - _dbg_assert_(rdcnt < FD_SETSIZE); - _dbg_assert_(wrcnt < FD_SETSIZE); - _dbg_assert_(excnt < FD_SETSIZE); - - if (rdcnt + wrcnt + excnt == 0) { - // Nothing to check: a select() used as a sleep. Winsock's select() fails on empty sets. + if (count == 0) { + // Nothing to check: a select() used as a sleep. return { 0, true, EAGAIN }; } - timeval tmout = { 0, 0 }; - int retval = select(maxHostSocket + 1, readfds ? &rdfds : nullptr, writefds ? &wrfds : nullptr, exceptfds ? &exfds : nullptr, &tmout); + const int retval = CheckHostSockets(checks, count); if (retval == 0) { return { 0, true, EAGAIN }; } @@ -599,18 +640,16 @@ static InetTry TrySelect(SceUID threadID, const u32 *args, bool firstTry) { } // Convert the results back to PSP fd_sets. - for (auto &set : sets) { - if (set.set) { - NetInetFD_ZERO(set.set); + for (auto *set : sets) { + if (set) { + NetInetFD_ZERO(set); } } - for (int i = SocketManager::MIN_VALID_INET_SOCKET; i < nfds; i++) { - if (hostSockets[i] == 0) { - continue; - } - for (auto &set : sets) { - if (set.set && FD_ISSET(hostSockets[i], set.hostSet)) { - NetInetFD_SET(i, set.set); + for (int i = 0; i < count; i++) { + const bool ready[3] = { checks[i].read, checks[i].write, checks[i].except }; + for (int s = 0; s < 3; s++) { + if (ready[s]) { + NetInetFD_SET(guestFds[i], sets[s]); } } } @@ -622,60 +661,49 @@ static InetTry TryPoll(SceUID threadID, const u32 *args) { SceNetInetPollfd *fdarray = (SceNetInetPollfd*)Memory::GetPointerOrException(args[0]); // SceNetInetPollfd/pollfd, sceNetInetPoll() have similarity to BSD poll() but pollfd have different size on 64bit const int nfds = (int)args[1]; - int maxHostFd = 0; + std::vector checks(nfds); + std::vector open(nfds); int hostCount = 0; - int closedCount = 0; - fd_set readfds{}, writefds{}, exceptfds{}; - FD_ZERO(&readfds); FD_ZERO(&writefds); FD_ZERO(&exceptfds); for (int i = 0; i < nfds; i++) { SOCKET hostSocket; fdarray[i].revents = 0; - if (!GetHostSocket(fdarray[i].fd, &hostSocket)) { - // Not open, reported as POLLNVAL below. - closedCount++; - continue; + open[i] = GetHostSocket(fdarray[i].fd, &hostSocket); + if (open[i]) { + const int events = fdarray[i].events; + checks[hostCount++] = { hostSocket, (events & (INET_POLLRDNORM | INET_POLLIN)) != 0, (events & (INET_POLLWRNORM | INET_POLLOUT)) != 0, (events & (INET_POLLRDBAND | INET_POLLPRI)) != 0 }; } - hostCount++; - if ((int)hostSocket > maxHostFd) { - maxHostFd = (int)hostSocket; - } - FD_SET(hostSocket, &readfds); - FD_SET(hostSocket, &writefds); - FD_SET(hostSocket, &exceptfds); } - if (hostCount == 0 && closedCount == 0) { - // Winsock's select() fails on empty sets. + if (nfds == 0) { return { 0, true, EAGAIN }; } - - if (hostCount > 0) { - timeval tmout = { 0, 0 }; - if (select(maxHostFd + 1, &readfds, &writefds, &exceptfds, &tmout) < 0) { - UpdateErrnoFromHost(threadID, EINTR, "sceNetInetPoll"); - return { -1, false, 0 }; - } + if (CheckHostSockets(checks.data(), hostCount) < 0) { + UpdateErrnoFromHost(threadID, socket_errno, "sceNetInetPoll"); + return { -1, false, 0 }; } int retval = 0; + int check = 0; for (int i = 0; i < nfds; i++) { - SOCKET hostSocket; - if (!GetHostSocket(fdarray[i].fd, &hostSocket)) { + if (!open[i]) { // As on hardware (pspautotests net/inet/poll). fdarray[i].revents = INET_POLLNVAL; retval++; continue; } - if ((fdarray[i].events & (INET_POLLRDNORM | INET_POLLIN)) && FD_ISSET(hostSocket, &readfds)) - fdarray[i].revents |= (INET_POLLRDNORM | INET_POLLIN); //POLLIN_SET - if ((fdarray[i].events & (INET_POLLWRNORM | INET_POLLOUT)) && FD_ISSET(hostSocket, &writefds)) - fdarray[i].revents |= (INET_POLLWRNORM | INET_POLLOUT); //POLLOUT_SET + // Only what was asked for: no POLLHUP or POLLERR, even for a peer that closed or reset the + // connection (pspautotests net/inet/polledge). + const HostSocketReady &ready = checks[check++]; + if (ready.read) + fdarray[i].revents |= (INET_POLLRDNORM | INET_POLLIN); + if (ready.write) + fdarray[i].revents |= (INET_POLLWRNORM | INET_POLLOUT); + if (ready.except) + fdarray[i].revents |= (INET_POLLRDBAND | INET_POLLPRI); fdarray[i].revents &= fdarray[i].events; - if (FD_ISSET(hostSocket, &exceptfds)) - fdarray[i].revents |= (INET_POLLRDBAND | INET_POLLPRI | INET_POLLERR); //POLLEX_SET; // Can be raised on revents regardless of events bitmask? if (fdarray[i].revents) retval++; - VERBOSE_LOG(Log::sceNet, "Poll Socket#%d Fd: %d, events: %04x, revents: %04x, availToRecv: %d", i, fdarray[i].fd, fdarray[i].events, fdarray[i].revents, (int)getAvailToRecv(fdarray[i].fd)); + VERBOSE_LOG(Log::sceNet, "Poll Socket#%d Fd: %d, events: %04x, revents: %04x", i, fdarray[i].fd, fdarray[i].events, fdarray[i].revents); } if (retval == 0) { return { 0, true, EAGAIN }; @@ -855,8 +883,8 @@ int sceNetInetSelect(int nfds, u32 readfdsPtr, u32 writefdsPtr, u32 exceptfdsPtr int sceNetInetPoll(MIPSState *mips, u32 fdsPtr, u32 nfds, int timeout) { DEBUG_LOG(Log::sceNet, "UNTESTED sceNetInetPoll(%08x, %d, %i) at %08x", fdsPtr, nfds, timeout, mips->pc); InetThreadSeen(); - if (nfds > FD_SETSIZE) - nfds = FD_SETSIZE; + if (nfds > PSP_NET_INET_FD_SETSIZE) + nfds = PSP_NET_INET_FD_SETSIZE; if (!InetValidRange(fdsPtr, nfds * (u32)sizeof(SceNetInetPollfd), __FUNCTION__)) { return hleLogError(Log::sceNet, -1, "bad fds %08x", fdsPtr); } @@ -1305,13 +1333,8 @@ static InetTry TryConnectDone(SceUID threadID, const u32 *args) { UpdateErrnoFromHost(threadID, EBADF, "sceNetInetConnect"); return { -1, false, 0 }; } - fd_set wrfds, exfds; - FD_ZERO(&wrfds); - FD_ZERO(&exfds); - FD_SET(inetSock->sock, &wrfds); - FD_SET(inetSock->sock, &exfds); - timeval tmout = { 0, 0 }; - int ready = select((int)inetSock->sock + 1, nullptr, &wrfds, &exfds, &tmout); + HostSocketReady check = { inetSock->sock, false, true, true }; + int ready = CheckHostSockets(&check, 1); if (ready == 0) { return { -1, true, EINPROGRESS }; } diff --git a/test.py b/test.py index 854396a28b..51637cef1f 100755 --- a/test.py +++ b/test.py @@ -357,6 +357,7 @@ tests_good = [ "net/inet/addr", "net/inet/options", "net/inet/limits", + "net/inet/polledge", "misc/libc", "misc/sdkver", "misc/testgp", From cc1fad6e2a6845e3ec003c9b8fc3b826dd6aa2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 28 Oct 2026 11:42:58 +0100 Subject: [PATCH 2/2] sceNetInet: Check up to 64 sockets at once on Windows, as before Splitting larger checks into groups of 64 for Winsock's select() was untested, and no game is known to poll more sockets than that. Past 64, it now logs an error and checks the first 64, as the old code did. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceNetInet.cpp | 62 ++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/Core/HLE/sceNetInet.cpp b/Core/HLE/sceNetInet.cpp index 1fe11d80b2..aa29434f3d 100644 --- a/Core/HLE/sceNetInet.cpp +++ b/Core/HLE/sceNetInet.cpp @@ -534,39 +534,43 @@ struct HostSocketReady { }; // Returns how many of the checks came back ready, like select(), or -1 with socket_errno set. -// Elsewhere select() can't take a descriptor numbered FD_SETSIZE or above, so this uses poll(), mapped -// as select() would see it. A hung-up socket counts as writable too: macOS reports a refused connect -// as just POLLHUP, which its select() shows as writable. Winsock's select() has no such limit, just one of FD_SETSIZE (64) -// sockets per set, so it's called on groups of that many. +// Outside Windows, select() can't take a descriptor numbered FD_SETSIZE or above, so this uses poll(), +// mapped as select() would see it. A hung-up socket counts as writable too: macOS reports a refused +// connect as just POLLHUP, which its select() shows as writable. static int CheckHostSockets(HostSocketReady *checks, int count) { int ready = 0; #if defined(_WIN32) - for (int start = 0; start < count; start += FD_SETSIZE) { - const int n = std::min(count - start, (int)FD_SETSIZE); - fd_set rd, wr, ex; - FD_ZERO(&rd); - FD_ZERO(&wr); - FD_ZERO(&ex); - bool anyRd = false, anyWr = false, anyEx = false; - for (int i = start; i < start + n; i++) { - if (checks[i].read) { FD_SET(checks[i].sock, &rd); anyRd = true; } - if (checks[i].write) { FD_SET(checks[i].sock, &wr); anyWr = true; } - if (checks[i].except) { FD_SET(checks[i].sock, &ex); anyEx = true; } - } - if (!anyRd && !anyWr && !anyEx) { - // Winsock's select() fails on empty sets. - continue; - } - timeval tmout = { 0, 0 }; - if (select(0, anyRd ? &rd : nullptr, anyWr ? &wr : nullptr, anyEx ? &ex : nullptr, &tmout) < 0) { - return -1; - } - for (int i = start; i < start + n; i++) { - checks[i].read = checks[i].read && FD_ISSET(checks[i].sock, &rd); - checks[i].write = checks[i].write && FD_ISSET(checks[i].sock, &wr); - checks[i].except = checks[i].except && FD_ISSET(checks[i].sock, &ex); - ready += checks[i].read + checks[i].write + checks[i].except; + if (count > FD_SETSIZE) { + // Winsock's select() takes 64 sockets a set. No game is known to need more. + ERROR_LOG(Log::sceNet, "Checking only %d of %d sockets", (int)FD_SETSIZE, count); + for (int i = FD_SETSIZE; i < count; i++) { + checks[i].read = checks[i].write = checks[i].except = false; } + count = FD_SETSIZE; + } + fd_set rd, wr, ex; + FD_ZERO(&rd); + FD_ZERO(&wr); + FD_ZERO(&ex); + bool anyRd = false, anyWr = false, anyEx = false; + for (int i = 0; i < count; i++) { + if (checks[i].read) { FD_SET(checks[i].sock, &rd); anyRd = true; } + if (checks[i].write) { FD_SET(checks[i].sock, &wr); anyWr = true; } + if (checks[i].except) { FD_SET(checks[i].sock, &ex); anyEx = true; } + } + if (!anyRd && !anyWr && !anyEx) { + // Winsock's select() fails on empty sets. + return 0; + } + timeval tmout = { 0, 0 }; + if (select(0, anyRd ? &rd : nullptr, anyWr ? &wr : nullptr, anyEx ? &ex : nullptr, &tmout) < 0) { + return -1; + } + for (int i = 0; i < count; i++) { + checks[i].read = checks[i].read && FD_ISSET(checks[i].sock, &rd); + checks[i].write = checks[i].write && FD_ISSET(checks[i].sock, &wr); + checks[i].except = checks[i].except && FD_ISSET(checks[i].sock, &ex); + ready += checks[i].read + checks[i].write + checks[i].except; } #else std::vector fds(count);