From 60a49ccfd2bddad0e407758bc1b135e3434eac17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 08:58:02 +0100 Subject: [PATCH 01/24] Ad hoc: Use the PSP's measured times instead of guessed ones Where pspautotests net/adhoc/timing or net/adhoc/pair/timing measured a value, use it: - Call costs: PdpSend 21 us (10 broadcast), PtpSend 122, PtpFlush 196, GetPdpStat 1, GetPtpStat 2, PollSocket 18, GetScanInfo 2, GetPeerList and MatchingGetMembers 4 (no longer a 100 us reschedule), MatchingStart 416 instead of 30 ms, sceNetAdhocctlTerm 154, sceNetAdhocTerm 10 and sceNetTerm 209 instead of 10 ms each, GameModeUpdateMaster 80, UpdateReplica 9. - Game mode data goes out every 12 ms, not every 0.5 ms. - DISCONNECT comes 640 ms after ExitGameMode, not 105 ms. - CONNECT after a Join no longer waits for a peer: a PSP delivers it before the host shows up in the peer list. Several of the old values were tuned for particular games (BattleZone's frame rate, Fate Unlimited Codes and Bleach through PollSocket), and may have been making up for other differences that are fixed now. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/proAdhoc.h | 3 +- Core/HLE/sceNet.cpp | 2 +- Core/HLE/sceNetAdhoc.cpp | 48 +++++++++++++++++--------------- Core/HLE/sceNetAdhocMatching.cpp | 7 +++-- Core/HLE/sceNetAdhocMatching.h | 2 +- 5 files changed, 34 insertions(+), 28 deletions(-) diff --git a/Core/HLE/proAdhoc.h b/Core/HLE/proAdhoc.h index fb62ee93ab..cae0f6ef30 100644 --- a/Core/HLE/proAdhoc.h +++ b/Core/HLE/proAdhoc.h @@ -46,7 +46,8 @@ // Default GameMode definitions #define ADHOC_GAMEMODE_PORT 31000 -#define GAMEMODE_UPDATE_INTERVAL 500 // 12000 usec on JPCSP, but lower value works better on BattleZone (in order to get full speed 60 FPS) +// A PSP sends game mode data about every 12 ms (pspautotests net/adhoc/pair/timing). +#define GAMEMODE_UPDATE_INTERVAL 12000 #define GAMEMODE_INIT_DELAY 10000 #define GAMEMODE_SYNC_TIMEOUT 250000 #define GAMEMODE_WAITID 0x2001 // Just to differentiate WaitID with other ID on WAITTYPE_NET diff --git a/Core/HLE/sceNet.cpp b/Core/HLE/sceNet.cpp index 4acba4ed36..f4ae6d5b5a 100644 --- a/Core/HLE/sceNet.cpp +++ b/Core/HLE/sceNet.cpp @@ -1010,7 +1010,7 @@ static u32 sceNetTerm() { int retval = Net_Term(); // Give time to make sure everything are cleaned up - hleEatMicro(adhocDefaultDelay); + hleEatMicro(209); return hleLogInfo(Log::sceNet, retval); } diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 83e5396c5d..a6d7d33f5d 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -99,6 +99,8 @@ int adhocctlPollEvent = -1; // When the game made its last request to the Adhoc Server, in emulated time. Its event isn't delivered // earlier than on a PSP (see PspEventDelayUs). static u64 adhocctlRequestTimeUs = 0; +// Whether the pending DISCONNECT comes from ExitGameMode, which takes longer. +static bool adhocctlDisconnectFromGameMode = false; // Whether the server has us in a group. The state the game sees only changes when the event is // delivered, so a Disconnect can come while a Create or Connect is still on its way. static bool adhocctlInServerGroup = false; @@ -116,7 +118,7 @@ static int PspEventDelayUs(u32 flags) { default: return getActivePeerCount() > 0 ? 3500000 : 4000000; } case ADHOCCTL_EVENT_SCAN: return 1015000; - case ADHOCCTL_EVENT_DISCONNECT: return 105000; + case ADHOCCTL_EVENT_DISCONNECT: return adhocctlDisconnectFromGameMode ? 640000 : 105000; default: return 0; } } @@ -2880,7 +2882,7 @@ int sceNetAdhocPdpSend(MIPSState *mips, int id, const char *mac, u32 port, void // Free Network Lock //_freeNetworkLock(); - hleEatMicro(50); // Can be longer than 1ms tho + hleEatMicro(21); // Sent Data if (sent >= 0) { DEBUG_LOG(Log::sceNet, "sceNetAdhocPdpSend[%i:%u]: Sent %u bytes to %s:%u\n", id, getLocalPort(pdpsocket.id), sent, ip2str(target.sin_addr).c_str(), ntohs(target.sin_port)); @@ -2987,7 +2989,7 @@ int sceNetAdhocPdpSend(MIPSState *mips, int id, const char *mac, u32 port, void // Free Network Lock //_freeNetworkLock(); - hleEatMicro(50); + hleEatMicro(10); // Success, Broadcast never fails! return 0; // len; } @@ -3586,8 +3588,7 @@ int sceNetAdhocPollSocket(MIPSState *mips, u32 socketStructAddr, int count, int } } } - // Workaround to get 30 FPS instead of the too fast 60 FPS on Fate Unlimited Codes, it's abit absurd for a non-blocking call to have this much delay tho, and hleDelayResult doesn't works as good as hleEatMicro for this workaround. - hleEatMicro(50); // hleEatMicro(7500); // normally 1ms, but using 7.5ms here seems to show better result for Bleach Heat the Soul 7 and other games with too high FPS, but may have a risk of slowing down games that already runs at normal FPS? (need more games to test this) + hleEatMicro(18); return hleLogDebug(Log::sceNet, affectedsockets, "success"); } //else if (nonblock && affectedsockets < 0) @@ -3842,7 +3843,7 @@ int sceNetAdhocctlGetScanInfo(MIPSState *mips, u32 sizeAddr, u32 bufAddr) { // Multithreading Unlock peerlock.unlock(); - hleEatMicro(200); + hleEatMicro(2); // Return Success return hleLogDebug(Log::sceNet, 0); } @@ -3892,15 +3893,17 @@ static u32 sceNetAdhocctlAddHandler(u32 handlerPtr, u32 handlerArg) { return hleNoLog(retval); } -u32 NetAdhocctl_Disconnect() { +static u32 NetAdhocctl_Disconnect(bool fromGameMode = false) { // Library initialized if (netAdhocctlInited) { int iResult, error; if (isAdhocctlBusy && CoreTiming::IsScheduled(adhocctlNotifyEvent)) { return SCE_NET_ADHOCCTL_ERROR_BUSY; } - // Returns at once on a PSP, and DISCONNECT follows after about 105 ms (pspautotests net/adhoc/timing). + // Returns at once on a PSP, and DISCONNECT follows after about 105 ms (pspautotests net/adhoc/timing), + // or 640 ms after ExitGameMode (net/adhoc/pair/timing). adhocctlRequestTimeUs = CoreTiming::GetGlobalTimeUs(); + adhocctlDisconnectFromGameMode = fromGameMode; // Connected State (Adhoc Mode). Attempting to leave a group while not in a group will be kicked out by Adhoc Server (ie. some games tries to disconnect more than once within a short time) if (adhocctlState != ADHOCCTL_STATE_DISCONNECTED || adhocctlInServerGroup) { @@ -4052,7 +4055,7 @@ int sceNetAdhocctlTerm() { //if (netAdhocMatchingInited) NetAdhocMatching_Term(); int retval = NetAdhocctl_Term(); - hleEatMicro(adhocDefaultDelay); + hleEatMicro(154); return hleLogInfo(Log::sceNet, retval); } @@ -4490,7 +4493,7 @@ int sceNetAdhocTerm() { // WLAN might be disabled in the middle of successfull multiplayer, but we still need to cleanup all the sockets right? int retval = NetAdhoc_Term(); - hleEatMicro(adhocDefaultDelay); + hleEatMicro(10); return hleLogInfo(Log::sceNet, retval); } @@ -4582,7 +4585,7 @@ static int sceNetAdhocGetPdpStat(MIPSState *mips, u32 structSize, u32 structAddr // Update Buffer Length *buflen = i * sizeof(SceNetAdhocPdpStat); - hleEatMicro(50); // Not sure how long it supposed to take + hleEatMicro(1); // Success return 0; } @@ -4705,7 +4708,7 @@ static int sceNetAdhocGetPtpStat(MIPSState *mips, u32 structSize, u32 structAddr // Update Buffer Length *buflen = i * sizeof(SceNetAdhocPtpStat); - hleEatMicro(50); // Not sure how long it takes, since GetPtpStat didn't get logged when using prx files on JPCSP + hleEatMicro(2); // Success return 0; } @@ -5873,7 +5876,7 @@ static int sceNetAdhocPtpSend(MIPSState *mips, int id, u32 dataAddr, u32 dataSiz sent = ptp_send_postoffice(id - 1, data, len); if (sent == 0) { // sent - hleEatMicro(50); + hleEatMicro(122); return 0; } if (sent == SOCKET_ERROR) { @@ -5893,7 +5896,7 @@ static int sceNetAdhocPtpSend(MIPSState *mips, int id, u32 dataAddr, u32 dataSiz // Success if (sent > 0) { - hleEatMicro(50); // mostly 1ms, sometimes 1~10ms ? doesn't seems to be switching to a different thread during this duration + hleEatMicro(122); // Save Length *len = sent; @@ -6137,7 +6140,7 @@ static int sceNetAdhocPtpFlush(MIPSState *mips, int id, int timeout, int nonbloc // Connected Socket if (ptpsocket.state == ADHOC_PTP_STATE_ESTABLISHED) { - hleEatMicro(50); + hleEatMicro(196); // There are two ways to flush, you can either set TCP_NODELAY to 1 or TCP_CORK to 0. // Apply Send Timeout Settings to Socket setSockTimeout(ptpsocket.id, SO_SNDTIMEO, timeout); @@ -6304,7 +6307,7 @@ static int sceNetAdhocGameModeUpdateMaster(MIPSState *mips) { gma.dataSent = 0; } - hleEatMicro(100); + hleEatMicro(80); return 0; } @@ -6383,7 +6386,7 @@ static int sceNetAdhocGameModeUpdateReplica(MIPSState *mips, int id, u32 infoAdd } } - hleEatMicro(100); + hleEatMicro(9); return hleLogDebug(Log::sceNet, 0); } @@ -6444,8 +6447,9 @@ void __NetTriggerCallbacks() args[1] = error; u64 now = (u64)(time_now_d() * 1000000.0); - // FIXME: When Joining a group, Do we need to wait for group creator's peer data before triggering the callback to make sure the game not to thinks we're the group creator? - if ((flags != ADHOCCTL_EVENT_CONNECT && flags != ADHOCCTL_EVENT_GAME) || adhocConnectionType != ADHOC_JOIN || getActivePeerCount() > 0 || static_cast(now - adhocctlStartTime) > adhocDefaultTimeout) + // A PSP delivers CONNECT after a Join before the host shows up in its peer list (pspautotests + // net/adhoc/pair/timing), so only GAME waits for a peer here. + if (flags != ADHOCCTL_EVENT_GAME || adhocConnectionType != ADHOC_JOIN || getActivePeerCount() > 0 || static_cast(now - adhocctlStartTime) > adhocDefaultTimeout) { // Since 0 is a valid index to types_ we use -1 to detects if it was loaded from an old save state if (actionAfterAdhocMipsCall < 0) { @@ -6562,7 +6566,7 @@ int NetAdhocctl_ExitGameMode() { adhocctlCurrentMode = ADHOCCTL_MODE_NONE; netAdhocGameModeEntered = false; - return NetAdhocctl_Disconnect(); + return NetAdhocctl_Disconnect(true); } static int sceNetAdhocctlExitGameMode(MIPSState *mips) { @@ -6690,8 +6694,8 @@ static int sceNetAdhocctlGetPeerList(MIPSState *mips, u32 sizeAddr, u32 bufAddr) // Multithreading Unlock peerlock.unlock(); - // Return Success - return hleDelayResult(0, "delay 100 ~ 1000us", 100); // seems to have different thread running within the delay duration + hleEatMicro(4); + return 0; } // Invalid Arguments diff --git a/Core/HLE/sceNetAdhocMatching.cpp b/Core/HLE/sceNetAdhocMatching.cpp index c906061129..34f1d8cef0 100644 --- a/Core/HLE/sceNetAdhocMatching.cpp +++ b/Core/HLE/sceNetAdhocMatching.cpp @@ -45,7 +45,8 @@ std::vector matchingThreads; std::deque matchingEvents; bool netAdhocMatchingInited; -int adhocMatchingEventDelay = 30000; //30000 +// What MatchingStart takes on a PSP (pspautotests net/adhoc/timing). +int adhocMatchingEventDelay = 416; void DoNetAdhocMatchingInited(PointerWrap &p) { Do(p, netAdhocMatchingInited); @@ -2524,8 +2525,8 @@ static int sceNetAdhocMatchingGetMembers(int matchingId, u32 sizeAddr, u32 buf) DEBUG_LOG(Log::sceNet, "MemberList [Requested: %i][Discovered: %i]", requestedpeers, filledpeers); } - // Return Success - return hleDelayResult(hleLogDebug(Log::sceNet, 0), "delay 100 ~ 1000us", 100); // seems to have different thread running within the delay duration + hleEatMicro(4); + return hleLogDebug(Log::sceNet, 0); } // Gran Turismo may replace the 1st bit of the 1st byte of MAC address's OUI with 0 (unicast bit), or replace the whole 6-bytes of MAC address with all 00 (invalid mac) for unknown reason diff --git a/Core/HLE/sceNetAdhocMatching.h b/Core/HLE/sceNetAdhocMatching.h index 156e8b57fc..23811d2595 100644 --- a/Core/HLE/sceNetAdhocMatching.h +++ b/Core/HLE/sceNetAdhocMatching.h @@ -42,4 +42,4 @@ void __NetAdhocMatchingInit(); void __NetAdhocMatchingShutdown(); extern bool netAdhocMatchingInited; -extern int adhocMatchingEventDelay; //30000 +extern int adhocMatchingEventDelay; From 08c76b8b25a66a8e0c5fbbfc0e17d202db656b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 09:09:45 +0100 Subject: [PATCH 02/24] sceNetAdhocGameMode: Create master and replicas as a PSP does pspautotests net/adhoc/pair/gamemode: - CreateMaster with size 0 fails with INVALID_DATALEN. - CreateReplica for a MAC that isn't in the game mode group fails with 0x80410D13. PPSSPP made one, and then blocked the thread forever waiting to sync with it. - CreateReplica for the same MAC again gives ALREADY_CREATED. The check compared the pointer instead of the MAC, so it never matched. - A replica's ID is the member's place in the member list, host first: the client's replica of the host is 0. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/ErrorCodes.h | 2 ++ Core/HLE/sceNetAdhoc.cpp | 31 ++++++++++++++++++++----------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/Core/HLE/ErrorCodes.h b/Core/HLE/ErrorCodes.h index 9af26b28df..5138c083b9 100644 --- a/Core/HLE/ErrorCodes.h +++ b/Core/HLE/ErrorCodes.h @@ -609,6 +609,8 @@ enum PSPErrorCode : u32 { SCE_NET_ADHOC_ERROR_ALREADY_CREATED = 0x8041071A, SCE_NET_ADHOC_ERROR_NOT_IN_GAMEMODE = 0x8041071B, SCE_NET_ADHOC_ERROR_NOT_CREATED = 0x8041071C, + // GameModeCreateReplica for a MAC that isn't in the game mode group. The name is ours. + SCE_NET_ADHOC_ERROR_NOT_GAMEMODE_MEMBER = 0x80410D13, // pspnet_adhoc_matching SCE_NET_ADHOC_MATCHING_ERROR_INVALID_MODE = 0x80410801, diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index a6d7d33f5d..455d993985 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -6194,6 +6194,10 @@ static int sceNetAdhocGameModeCreateMaster(MIPSState *mips, u32 dataAddr, int si if (size < 0 || !Memory::IsValidAddress(dataAddr)) return hleLogError(Log::sceNet, SCE_NET_ADHOCCTL_ERROR_INVALID_ARG, "invalid arg"); + // pspautotests net/adhoc/pair/gamemode + if (size == 0) + return hleLogError(Log::sceNet, SCE_NET_ADHOC_ERROR_INVALID_DATALEN, "invalid data length"); + if (masterGameModeArea.data) return hleLogError(Log::sceNet, SCE_NET_ADHOC_ERROR_ALREADY_CREATED, "already created"); // FIXME: Should we return a success instead? (need to test this on a homebrew) @@ -6247,18 +6251,23 @@ static int sceNetAdhocGameModeCreateReplica(MIPSState *mips, const char *mac, u3 if (mac == nullptr || size < 0 || !Memory::IsValidAddress(dataAddr)) return hleLogError(Log::sceNet, SCE_NET_ADHOCCTL_ERROR_INVALID_ARG, "invalid arg"); + const SceNetEtherAddr &replicaMac = *(const SceNetEtherAddr *)mac; + // The ID is the member's place in the member list (the host first), and a MAC that isn't a member is + // refused. With two consoles, the host's replica of the client is 1 and the client's of the host 0 + // (pspautotests net/adhoc/pair/gamemode). + auto member = std::find_if(gameModeMacs.begin(), gameModeMacs.end(), [&](const SceNetEtherAddr &m) { return IsMatch(m, replicaMac); }); + if (member == gameModeMacs.end()) + return hleLogError(Log::sceNet, SCE_NET_ADHOC_ERROR_NOT_GAMEMODE_MEMBER, "not a member"); + int replicaId = (int)(member - gameModeMacs.begin()); + hleEatMicro(1000); - int maxid = 0; auto it = std::find_if(replicaGameModeAreas.begin(), replicaGameModeAreas.end(), - [mac, &maxid](GameModeArea const& e) { - if (e.id > maxid) maxid = e.id; - return IsMatch(e.mac, mac); + [&](GameModeArea const& e) { + return IsMatch(e.mac, replicaMac); }); - // MAC address already existed! - if (it != replicaGameModeAreas.end()) { - WARN_LOG(Log::sceNet, "sceNetAdhocGameModeCreateReplica - [%s] is already existed (id: %d)", mac2str((SceNetEtherAddr*)mac).c_str(), it->id); - return it->id; // SCE_NET_ADHOC_ERROR_ALREADY_CREATED - } + // pspautotests net/adhoc/pair/gamemode + if (it != replicaGameModeAreas.end()) + return hleLogError(Log::sceNet, SCE_NET_ADHOC_ERROR_ALREADY_CREATED, "already created"); int ret = 0; gameModeBuffSize = std::max(gameModeBuffSize, size); @@ -6270,9 +6279,9 @@ static int sceNetAdhocGameModeCreateReplica(MIPSState *mips, const char *mac, u3 if (data) { Memory::Memcpy(data, dataAddr, size); //int sock = sceNetAdhocPdpCreate(mac, ADHOC_GAMEMODE_PORT, size, 0); - GameModeArea gma = { maxid + 1, size, dataAddr, CoreTiming::GetGlobalTimeUsScaled(), 0, 0, *(SceNetEtherAddr*)mac, data }; + GameModeArea gma = { replicaId, size, dataAddr, CoreTiming::GetGlobalTimeUsScaled(), 0, 0, replicaMac, data }; replicaGameModeAreas.push_back(gma); - ret = gma.id; // Valid id for replica is higher than 0? + ret = gma.id; // Block current thread to sync initial master data after Master and all Replicas have been created if (masterGameModeArea.data != NULL && replicaGameModeAreas.size() == (gameModeMacs.size() - 1)) { From 882eee4c13f7ca265fd6fd8a57e609559a16ddf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 09:09:45 +0100 Subject: [PATCH 03/24] sceNetAdhocMatching: A denied P2P request goes back to a known peer CancelTarget on an incoming P2P request marked the peer timed out. It only escaped a TIMEOUT if one of its hellos came in before the next timeout check, which it no longer does now that timeouts are checked all the time. On a PSP it's a peer known from its hellos again, with no TIMEOUT (pspautotests net/adhoc/pair/matching3). Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceNetAdhocMatching.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Core/HLE/sceNetAdhocMatching.cpp b/Core/HLE/sceNetAdhocMatching.cpp index 34f1d8cef0..21d55ddb1a 100644 --- a/Core/HLE/sceNetAdhocMatching.cpp +++ b/Core/HLE/sceNetAdhocMatching.cpp @@ -2188,6 +2188,16 @@ int NetAdhocMatching_CancelTargetWithOpt(int matchingId, const char* macAddress, { const bool notifyDeath = context->mode == PSP_ADHOC_MATCHING_MODE_PARENT && peer->state == PSP_ADHOC_MATCHING_PEER_CHILD && countConnectedPeers(context) > 1; + // A P2P peer whose request is denied goes back to one known from its hellos: no TIMEOUT, and it + // can ask again (pspautotests net/adhoc/pair/matching3). + if (context->mode == PSP_ADHOC_MATCHING_MODE_P2P && peer->state == PSP_ADHOC_MATCHING_PEER_INCOMING_REQUEST) { + sendCancelMessage(context, peer, optLen, opt); + peer->state = PSP_ADHOC_MATCHING_PEER_OFFER; + peer->lastping = CoreTiming::GetGlobalTimeUsScaled(); + hleEatCycles(adhocDefaultDelay); + return hleLogDebug(Log::sceNet, 0); + } + // Mark Peer as Canceled peer->state = PSP_ADHOC_MATCHING_PEER_CANCEL_IN_PROGRESS; From 3c9c01048a5b7aa3c1a379c6d862c1756183702a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 09:43:59 +0100 Subject: [PATCH 04/24] sceNetAdhocctl: Time GAMEMODE and leaving game mode as a PSP does GAMEMODE comes 4.38 s after CreateEnterGameMode, or 1.35 s after JoinEnterGameMode or the last member's arrival, whichever is later, instead of a fixed 2 s after the last member (plus 60 ms for a member, a workaround for Pocket Pool). DISCONNECT comes 650 ms after ExitGameMode. Measured in pspautotests net/adhoc/pair/gamemodetiming. A member that joins early still gets GAMEMODE before the host: it doesn't know when the host entered game mode. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceNetAdhoc.cpp | 18 +++++++++++------- pspautotests | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 455d993985..f80a69000a 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -118,7 +118,10 @@ static int PspEventDelayUs(u32 flags) { default: return getActivePeerCount() > 0 ? 3500000 : 4000000; } case ADHOCCTL_EVENT_SCAN: return 1015000; - case ADHOCCTL_EVENT_DISCONNECT: return adhocctlDisconnectFromGameMode ? 640000 : 105000; + case ADHOCCTL_EVENT_DISCONNECT: return adhocctlDisconnectFromGameMode ? 650000 : 105000; + // GAMEMODE comes 4.38 s after CreateEnterGameMode, or 1.35 s after JoinEnterGameMode, whichever is + // later, to both at once (net/adhoc/pair/gamemodetiming). + case ADHOCCTL_EVENT_GAME: return adhocConnectionType == ADHOC_JOIN ? 1350000 : 4380000; default: return 0; } } @@ -3901,7 +3904,7 @@ static u32 NetAdhocctl_Disconnect(bool fromGameMode = false) { return SCE_NET_ADHOCCTL_ERROR_BUSY; } // Returns at once on a PSP, and DISCONNECT follows after about 105 ms (pspautotests net/adhoc/timing), - // or 640 ms after ExitGameMode (net/adhoc/pair/timing). + // or 650 ms after ExitGameMode (net/adhoc/pair/gamemodetiming). adhocctlRequestTimeUs = CoreTiming::GetGlobalTimeUs(); adhocctlDisconnectFromGameMode = fromGameMode; @@ -6482,11 +6485,12 @@ void __NetTriggerCallbacks() case ADHOCCTL_EVENT_GAME: { newState = ADHOCCTL_STATE_GAMEMODE; - delayus = adhocEventDelay; - // TODO: Use blocking PTP connection to sync the timing just like official prx did (which is done before notifying user-defined Adhocctl Handlers) - // Workaround: Extra delay to prevent Joining player to progress faster than the Creator on Pocket Pool, but unbalanced delays could cause an issue on Shaun White Snowboarding :( - if (adhocConnectionType == ADHOC_JOIN) - delayus += adhocExtraDelay * 3; + delayus = RemainingEventDelayUs(flags); + // This is raised when the last member has arrived. The host's GAMEMODE also comes no sooner than + // 1.35 s after that, as the member's own does. A member doesn't know when the host called + // CreateEnterGameMode, so if it joins early it gets GAMEMODE before the host does. + if (adhocConnectionType != ADHOC_JOIN) + delayus = std::max(delayus, 1350000); // Shows player list INFO_LOG(Log::sceNet, "GameMode - All players have joined:"); int i = 0; diff --git a/pspautotests b/pspautotests index a340d72b02..46cdf2c7bc 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit a340d72b0243a5bd417d6bfe78988155bbe1ad11 +Subproject commit 46cdf2c7bcfa35d6ded0142cca64025309037601 From 8bb1f8462f5a2876ed52e3bf9bc3f91290d681ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 09:49:12 +0100 Subject: [PATCH 05/24] sceNetAdhoc: A PTP connection is established as soon as it's up GetPtpStat only looked at a connection in progress 33 ms after the last connect attempt, and PollSocket reported CONNECT 1 ms after it whether the connection was up or not. On a PSP both show it the moment it's up, about 5 ms after the connect (pspautotests net/adhoc/pair/ptpconnect). The old timings were tuned for GvG Next Plus, Bleach and Fate; Fate's network match still works. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceNetAdhoc.cpp | 49 ++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index f80a69000a..0e376e3cb2 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -862,6 +862,27 @@ static void ptpSetEstablished(AdhocSocket *sock) { sock->ptpEstablishedAt = CoreTiming::GetGlobalTimeUsScaled(); } +// A connection in progress shows up as established in GetPtpStat, and PollSocket reports CONNECT, as +// soon as it's up (pspautotests net/adhoc/pair/ptpconnect). +static void ptpUpdateConnecting(AdhocSocket *sock) { + if (sock->data.ptp.state != ADHOC_PTP_STATE_SYN_SENT && sock->data.ptp.state != ADHOC_PTP_STATE_SYN_RCVD) { + return; + } + if (serverHasRelay) { + if (sock->postofficeHandle != NULL) { + ptpSetEstablished(sock); + } + } else if (IsSocketReady(sock->data.ptp.id, true, true) > 0) { + struct sockaddr_in sin; + socklen_t sinlen = sizeof(sin); + memset(&sin, 0, sinlen); + // Ensure that the connection really established or not, since "select" alone can't accurately detects it + if (getpeername(sock->data.ptp.id, (struct sockaddr*)&sin, &sinlen) != SOCKET_ERROR) { + ptpSetEstablished(sock); + } + } +} + // A PSP checks the age of a connection's oldest unacknowledged data every rexmt_int, counting from when // the connection was established, and drops the connection at the first check where that data is older // than (rexmt_cnt + 1) * rexmt_int (pspautotests net/adhoc/pair/vanishtiming). Data sent once the peer @@ -3481,10 +3502,8 @@ int PollAdhocSocket(SceNetAdhocPollSd* sds, int count, int timeout, int nonblock sds[i].revents = (ADHOC_EV_SEND | ADHOC_EV_CONNECT | ADHOC_EV_FLUSH | (readable ? ADHOC_EV_RECV : 0)) & sds[i].events; } } else if (sock->type == SOCK_PTP) { - // Fate Unlimited Codes and Carnage Heart EXA relies on AdhocPollSocket in order to retry a failed PtpConnect, but the interval must not be too long (about 1 frame before state became Established by GetPtpStat) for Bleach Heat the Soul 7 to work properly. - if ((sds[i].events & ADHOC_EV_CONNECT) && ((sock->data.ptp.state == ADHOC_PTP_STATE_CLOSED && sock->attemptCount == 0) || - (sock->data.ptp.state == ADHOC_PTP_STATE_SYN_SENT && (static_cast(CoreTiming::GetGlobalTimeUsScaled() - sock->lastAttempt) > 1000/*std::max(1000, sock->retry_interval - 60000)*/)))) { - + ptpUpdateConnecting(sock); + if ((sds[i].events & ADHOC_EV_CONNECT) && sock->data.ptp.state == ADHOC_PTP_STATE_ESTABLISHED) { sds[i].revents |= ADHOC_EV_CONNECT; } // Check for socket state (already disconnected/closed by remote peer, already closed/deleted, not a socket or not opened/connected yet?) @@ -4646,27 +4665,7 @@ static int sceNetAdhocGetPtpStat(MIPSState *mips, u32 structSize, u32 structAddr // Valid Socket Entry auto sock = adhocSockets[j]; if ( sock != NULL && sock->type == SOCK_PTP) { - // Update connection state. - // GvG Next Plus relies on GetPtpStat to determine if Connection has been Established or not, but should not be updated too long for GvG to work, and should not be updated too fast(need to be 1 frame after PollSocket checking for ADHOC_EV_CONNECT) for Bleach Heat the Soul 7 to work properly. - if ((sock->data.ptp.state == ADHOC_PTP_STATE_SYN_SENT || sock->data.ptp.state == ADHOC_PTP_STATE_SYN_RCVD) && (static_cast(CoreTiming::GetGlobalTimeUsScaled() - sock->lastAttempt) > 33333/*sock->retry_interval*/)) { - // FIXME: May be we should poll all of them together on a single poll call instead of each socket separately? - if (serverHasRelay) { - if (sock->postofficeHandle != NULL) { - // good to go - ptpSetEstablished(sock); - } - } else { - if (IsSocketReady(sock->data.ptp.id, true, true) > 0) { - struct sockaddr_in sin; - socklen_t sinlen = sizeof(sin); - memset(&sin, 0, sinlen); - // Ensure that the connection really established or not, since "select" alone can't accurately detects it - if (getpeername(sock->data.ptp.id, (struct sockaddr*)&sin, &sinlen) != SOCKET_ERROR) { - ptpSetEstablished(sock); - } - } - } - } + ptpUpdateConnecting(sock); if (serverHasRelay) { if (sock->postofficeHandle != NULL) { From 478d10ff191ad6b015c6214aafbaf39db5d2b8e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 09:57:56 +0100 Subject: [PATCH 06/24] sceNetAdhoc: A PTP connection only gives up with data outstanding When the peer has left, a PSP drops the connection once data sent to it has gone unacknowledged for (rexmt_cnt + 1) * rexmt_int. A connection that only receives never fails. Count from the first data the peer couldn't acknowledge: sent after it left, or within 10 ms before (pspautotests net/adhoc/pair/vanishtiming). Co-Authored-By: Claude Opus 5.5 --- Core/HLE/proAdhoc.h | 2 ++ Core/HLE/sceNetAdhoc.cpp | 30 +++++++++++++++++++++++++----- pspautotests | 2 +- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Core/HLE/proAdhoc.h b/Core/HLE/proAdhoc.h index cae0f6ef30..28be1d4e66 100644 --- a/Core/HLE/proAdhoc.h +++ b/Core/HLE/proAdhoc.h @@ -327,6 +327,8 @@ typedef struct AdhocSocket { u64 lastAttempt; // timestamp to retry again (attempted by the game) u64 internalLastAttempt; // timestamp to retry again (internal use only) u64 ptpEstablishedAt; // when a PTP connection was established, which sets the phase of its timeout checks + u64 ptpLastSendAt; // the last PTP send + u64 ptpSendAfterPeerLeftAt; // the first PTP send after the peer left the group, which it never acknowledges bool isClient; // true if the game is using local port 0 when creating the socket union { SceNetAdhocPdpStat pdp; diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 0e376e3cb2..19f85d13df 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -885,23 +885,42 @@ static void ptpUpdateConnecting(AdhocSocket *sock) { // A PSP checks the age of a connection's oldest unacknowledged data every rexmt_int, counting from when // the connection was established, and drops the connection at the first check where that data is older -// than (rexmt_cnt + 1) * rexmt_int (pspautotests net/adhoc/pair/vanishtiming). Data sent once the peer -// has left is never acknowledged. +// than (rexmt_cnt + 1) * rexmt_int. A connection that only receives never fails +// (pspautotests net/adhoc/pair/vanishtiming). static bool ptpPeerGone(const AdhocSocket *sock) { u64 leftAt = peerLeftAt(&sock->data.ptp.paddr); if (leftAt == 0) { return false; } + // The oldest data the peer can't have acknowledged: sent after it left, or so shortly before that the + // acknowledgement couldn't come back (a round trip takes about 7 ms on a PSP). + u64 unackedSince = sock->ptpSendAfterPeerLeftAt; + if (sock->ptpLastSendAt != 0 && sock->ptpLastSendAt + 10000 >= leftAt && sock->ptpLastSendAt < leftAt) { + unackedSince = sock->ptpLastSendAt; + } + if (unackedSince == 0) { + return false; + } u64 interval = (u64)std::max(1, sock->retry_interval); u64 limit = interval * (u64)(std::max(0, sock->retry_count) + 1); u64 now = CoreTiming::GetGlobalTimeUsScaled(); - if (now - leftAt <= limit) { + if (now - unackedSince <= limit) { return false; } // The last check, at or before now, has to be past the limit. - u64 base = std::min(sock->ptpEstablishedAt, leftAt); + u64 base = std::min(sock->ptpEstablishedAt, unackedSince); u64 lastCheck = base + (now - base) / interval * interval; - return lastCheck > leftAt + limit; + return lastCheck > unackedSince + limit; +} + +static void ptpNoteSend(AdhocSocket *sock) { + u64 now = CoreTiming::GetGlobalTimeUsScaled(); + if (peerLeftAt(&sock->data.ptp.paddr) == 0) { + sock->ptpSendAfterPeerLeftAt = 0; + } else if (sock->ptpSendAfterPeerLeftAt == 0) { + sock->ptpSendAfterPeerLeftAt = now; + } + sock->ptpLastSendAt = now; } static int pdp_recv_postoffice(int idx, SceNetEtherAddr *saddr, uint16_t *sport, void *data, int *len) { @@ -1213,6 +1232,7 @@ static int ptp_send_postoffice(int idx, const void *data, int *len) { } int ptp_send_status = ptp_send(internal->postofficeHandle, (const char *)data, *len, true); + ptpNoteSend(internal); if (ptp_send_status == AEMU_POSTOFFICE_CLIENT_SESSION_DEAD) { // the session is dead, need to be reflected to the other side // this is not necessarily a relay failure, could simply be the other side disconnecting diff --git a/pspautotests b/pspautotests index 46cdf2c7bc..c09503e66a 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit 46cdf2c7bcfa35d6ded0142cca64025309037601 +Subproject commit c09503e66a68ea513af6ef976a114ead1f98bed5 From 0d686c1a959af464528899c74d9157e8d1d254fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 10:21:17 +0100 Subject: [PATCH 07/24] sceNetAdhocGameMode: Send the master every period, changed or not A PSP sends its master every 12 ms after the initial sync, and a peer's replica reports each one as an update. PPSSPP only sent a master that had changed, so a replica polled until updated waited for the next change. Also take every packet waiting each period, not one: every member sends each period. Measured in pspautotests net/adhoc/pair/gamemodesync. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceNetAdhoc.cpp | 17 ++++++++++++++--- pspautotests | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 19f85d13df..a4104cbed6 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -457,6 +457,14 @@ static void __GameModeNotify(u64 userdata, int cyclesLate) { return; } + // After the initial sync, a PSP sends the master every period whether it changed or not, and the + // peers' replicas report each one as an update (pspautotests net/adhoc/pair/gamemodesync). + if (__KernelGetWaitID(threadID, WAITTYPE_NET, error) != GAMEMODE_WAITID || error != 0) { + masterGameModeArea.dataUpdated = 1; + for (auto &gma : replicaGameModeAreas) + gma.dataSent = 0; + } + // Send Master data if (masterGameModeArea.dataUpdated) { int sentcount = 0; @@ -524,13 +532,16 @@ static void __GameModeNotify(u64 userdata, int cyclesLate) { } } - // Recv new Replica data when available - if (serverHasRelay || IsSocketReady(sock->data.pdp.id, true, false) > 0) { + // Recv new Replica data when available. Every member sends every period, so take all that's there. + for (int packets = 0; packets < ADHOCCTL_GAMEMODE_MAX_MEMBERS * 2 && (serverHasRelay || IsSocketReady(sock->data.pdp.id, true, false) > 0); ++packets) { SceNetEtherAddr sendermac; s32_le senderport = ADHOC_GAMEMODE_PORT; s32_le bufsz = gameModeBuffSize; int ret = sceNetAdhocPdpRecv(currentMIPS, gameModeSocket, &sendermac, &senderport, gameModeBuffer, &bufsz, 0, ADHOC_F_NONBLOCK); - if (ret >= 0 && bufsz > 0) { + if (ret < 0 || bufsz <= 0) { + break; + } + { // Shows a warning if the sender/source port is different than what it supposed to be. if (senderport != ADHOC_GAMEMODE_PORT && senderport != gameModePeerPorts[sendermac]) { char name[9] = {}; diff --git a/pspautotests b/pspautotests index c09503e66a..7f918e6b2f 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit c09503e66a68ea513af6ef976a114ead1f98bed5 +Subproject commit 7f918e6b2f9e9d289b864c010fa1fdb796319fa3 From d0d6f0f61b91811b7baf4e1eef7c1a5c8049ecea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 16:49:07 +0100 Subject: [PATCH 08/24] sceNetAdhocMatching: A child lists its parent first after a keepalive - Fixes #22331 A child's GetMembers starts with itself, then switches to the parent first one keepalive interval after it was accepted (net/adhoc/pair/matchingorder, recorded on two PSPs). We kept it self first. Bomberman's client checks that the first member is the host before it marks itself ready, so its lobby stayed empty and the host's start failed with "failed to connect to the game". Co-Authored-By: Claude Opus 5.5 --- Core/HLE/proAdhoc.h | 4 ++++ Core/HLE/sceNetAdhocMatching.cpp | 14 +++++++++++--- pspautotests | 2 +- test.py | 1 + 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Core/HLE/proAdhoc.h b/Core/HLE/proAdhoc.h index 28be1d4e66..617eb75f78 100644 --- a/Core/HLE/proAdhoc.h +++ b/Core/HLE/proAdhoc.h @@ -507,6 +507,10 @@ typedef struct SceNetAdhocMatchingContext { // Timeout u64_le timeout; + // Child: when GetMembers starts listing the parent first, one keepalive interval after being accepted + // (pspautotests net/adhoc/pair/matchingorder). 0 until accepted. + u64 parentFirstAt = 0; + // Helper Thread (fake PSP Thread) needed to execute callback //HLEHelperThread *matchingThread; int matching_thid; diff --git a/Core/HLE/sceNetAdhocMatching.cpp b/Core/HLE/sceNetAdhocMatching.cpp index 21d55ddb1a..00a8f46108 100644 --- a/Core/HLE/sceNetAdhocMatching.cpp +++ b/Core/HLE/sceNetAdhocMatching.cpp @@ -850,6 +850,7 @@ void actOnAcceptPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * s // Add Self Peer to the following position (using peer->state = 0 to identify as Self) addMember(context, &context->mac); + context->parentFirstAt = CoreTiming::GetGlobalTimeUsScaled() + context->keepalive_int; } // Spawn Accept Event @@ -2422,8 +2423,11 @@ static int sceNetAdhocMatchingGetMembers(int matchingId, u32 sizeAddr, u32 buf) int filledpeers = 0; if (requestedpeers > 0) { - // The console itself comes first, also for a client (pspautotests net/adhoc/pair/matching) - { + // The console itself comes first, also for a child until a keepalive interval after it was + // accepted. Then the parent does, and the child is listed among the siblings + // (pspautotests net/adhoc/pair/matching, net/adhoc/pair/matchingorder). + bool parentFirst = context->mode == PSP_ADHOC_MATCHING_MODE_CHILD && context->parentFirstAt != 0 && CoreTiming::GetGlobalTimeUsScaled() >= context->parentFirstAt && findParent(context) != NULL; + if (!parentFirst) { // Add Local MAC buf2[filledpeers++].mac_addr = context->mac; @@ -2512,8 +2516,12 @@ static int sceNetAdhocMatchingGetMembers(int matchingId, u32 sizeAddr, u32 buf) buf2[filledpeers++].mac_addr = peer->mac; DEBUG_LOG(Log::sceNet, "MemberSibling [%s]", mac2str(&peer->mac).c_str()); - } + } else if (parentFirst && peer->state == 0) { + // Self Peer + buf2[filledpeers++].mac_addr = peer->mac; + DEBUG_LOG(Log::sceNet, "MemberSelf [%s]", mac2str(&peer->mac).c_str()); + } } } sortedPeers.clear(); diff --git a/pspautotests b/pspautotests index 7f918e6b2f..8d093e7e67 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit 7f918e6b2f9e9d289b864c010fa1fdb796319fa3 +Subproject commit 8d093e7e67c65f5076913d0d09de2c34873b4679 diff --git a/test.py b/test.py index 6983b05cba..415c5c7e72 100755 --- a/test.py +++ b/test.py @@ -697,6 +697,7 @@ tests_next = [ "net/adhoc/pair/matching", "net/adhoc/pair/matching2", "net/adhoc/pair/matching3", + "net/adhoc/pair/matchingorder", "net/adhoc/pair/vanish", "sysmem/partition", # These two mbx tests only appeared to work because they papered over bugs From a8a63fa11ddea926375bdd7ef9c234864d6cede9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 19:20:06 +0100 Subject: [PATCH 09/24] sceNetAdhoc: Start socket IDs over at 1 after sceNetAdhocTerm A PSP hands out socket IDs from 1 again after sceNetAdhocTerm and sceNetAdhocInit (net/adhoc/socketids, recorded on a PSP). We kept counting up. Pangya: Fantasy Golf tears down the network between its room and the round, and then keeps using socket 1 without looking at what PdpCreate returned, so every send and receive failed with INVALID_SOCKET_ID and each player saw the other as "Off-line". Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceNetAdhoc.cpp | 11 +++++++---- pspautotests | 2 +- test.py | 1 + 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index a4104cbed6..b8cb9c3c15 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -374,6 +374,9 @@ static AdhocDataMode AdhocGetServerDataMode(std::string_view server) { int AcceptPtpSocket(int ptpId, int newsocket, sockaddr_in& peeraddr, SceNetEtherAddr* addr, u16_le* port); int PollAdhocSocket(SceNetAdhocPollSd* sds, int count, int timeout, int nonblock); static int FindFreeAdhocSocketSlot(); +// A PSP hands out socket IDs in increasing order (pspautotests net/adhoc/ptp), starting over from 1 +// after sceNetAdhocTerm (pspautotests net/adhoc/socketids). +static int nextAdhocSocketSlot = 0; int FlushPtpSocket(int socketId); int RecreatePtpSocket(int ptpId); int NetAdhocGameMode_DeleteMaster(); @@ -2289,6 +2292,7 @@ static void aemu_postoffice_server_log(const char *format, ...) { } void __NetAdhocInit() { + nextAdhocSocketSlot = 0; friendFinderRunning = false; netAdhocInited = false; netAdhocctlInited = false; @@ -2503,10 +2507,8 @@ static int pdp_create_postoffice(const SceNetEtherAddr *saddr, int sport, int bu } // Out of socket IDs is its own error (pspautotests net/adhoc/limits). -// A PSP hands out socket IDs in increasing order (pspautotests net/adhoc/ptp), so the search for a free -// slot starts after the last one handed out, and wraps around. Returns MAX_SOCKET when all are taken. -static int nextAdhocSocketSlot = 0; - +// The search for a free slot starts after the last one handed out, and wraps around (see +// nextAdhocSocketSlot). Returns MAX_SOCKET when all are taken. static int FindFreeAdhocSocketSlot() { for (int n = 0; n < MAX_SOCKET; n++) { int i = (nextAdhocSocketSlot + n) % MAX_SOCKET; @@ -4513,6 +4515,7 @@ int NetAdhoc_Term() { // Delete Adhoc Sockets deleteAllAdhocSockets(); + nextAdhocSocketSlot = 0; // Terminate Internet Library //sceNetInetTerm(); diff --git a/pspautotests b/pspautotests index 8d093e7e67..976112725f 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit 8d093e7e67c65f5076913d0d09de2c34873b4679 +Subproject commit 976112725faea2b417bc5f0fd206fd0ba2fb62a9 diff --git a/test.py b/test.py index 415c5c7e72..a79f18064d 100755 --- a/test.py +++ b/test.py @@ -680,6 +680,7 @@ tests_next = [ "net/adhoc/poll", "net/adhoc/ptp", "net/adhoc/self", + "net/adhoc/socketids", "net/inet/bind", "net/inet/poll", "net/inet/errors", From 6b14884e26d52a9935fb05ba33cf726c20d4c079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 20:06:28 +0100 Subject: [PATCH 10/24] sceNetAdhoc: sceNetAdhocTerm leaves adhocctl and the group alone A PSP stays in its group through sceNetAdhocTerm and sceNetAdhocInit, and matching keeps working; only the sockets go, and calls on them return NOT_INITIALIZED until the next init (net/adhoc/term, recorded on a PSP). We also terminated adhocctl, so a game that restarts the adhoc library inside a group found itself disconnected. Every other caller already terminates adhocctl itself first. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceNetAdhoc.cpp | 5 ++--- pspautotests | 2 +- test.py | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index b8cb9c3c15..424156d29f 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -4504,10 +4504,9 @@ int sceNetAdhocctlCreateEnterGameModeMin(MIPSState *mips, const char *group_name return hleLogDebug(Log::sceNet, NetAdhocctl_CreateEnterGameMode(group_name, game_type, num_members, membersAddr, timeout, flag)); } +// Leaves adhocctl, the group and matching running: a PSP stays in its group through sceNetAdhocTerm +// and sceNetAdhocInit (pspautotests net/adhoc/term). The other callers terminate adhocctl first. int NetAdhoc_Term() { - // Since Adhocctl & AdhocMatching uses Sockets & Threads we should terminate them also to release their resources - NetAdhocctl_Term(); - // Library is initialized if (netAdhocInited) { // Delete GameMode Buffers diff --git a/pspautotests b/pspautotests index 976112725f..212f5b3f57 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit 976112725faea2b417bc5f0fd206fd0ba2fb62a9 +Subproject commit 212f5b3f5776aa770add6cb96a5d2bf5973f6770 diff --git a/test.py b/test.py index a79f18064d..7afea4dfb4 100755 --- a/test.py +++ b/test.py @@ -681,6 +681,7 @@ tests_next = [ "net/adhoc/ptp", "net/adhoc/self", "net/adhoc/socketids", + "net/adhoc/term", "net/inet/bind", "net/inet/poll", "net/inet/errors", From 34a6ace9ae0c16338c19953e7ade24d4b9faca45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 20:18:19 +0100 Subject: [PATCH 11/24] sceNetAdhoc: Don't write the local MAC into the game's address argument PdpCreate, PtpOpen and PtpListen use the local MAC whatever the game passes (a stale MAC from an old savestate would otherwise break them), but they did it by overwriting the game's copy. A game that passes someone else's MAC from a table then has its own MAC in that table. pspautotests net/adhoc/pdp passes its "other" MAC to PdpCreate and later sends to it; every one of those sends came back to itself. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceNetAdhoc.cpp | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 424156d29f..1cac4bf46a 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -2610,9 +2610,12 @@ int NetAdhocPdpCreate(const char *mac, int port, int bufferSize, u32 flag, bool } SceNetEtherAddr * saddr = (SceNetEtherAddr *)mac; - // Some games (ie. DBZ Shin Budokai 2) might be getting the saddr/srcmac content from SaveState and causing problems :( So we try to fix it here + // Some games (ie. DBZ Shin Budokai 2) might be getting the saddr/srcmac content from SaveState and causing problems :( So we + // use the local MAC instead, without writing it into the game's memory. + SceNetEtherAddr localSrcMac; if (saddr != NULL) { - getLocalMac(saddr); + getLocalMac(&localSrcMac); + saddr = &localSrcMac; } @@ -4961,9 +4964,12 @@ static int sceNetAdhocPtpOpen(MIPSState *mips, const char *srcmac, int sport, co // Library is initialized if (netAdhocInited) { - // Some games (ie. DBZ Shin Budokai 2) might be getting the saddr/srcmac content from SaveState and causing problems if current MAC is different :( So we try to fix it here + // Some games (ie. DBZ Shin Budokai 2) might be getting the saddr/srcmac content from SaveState and causing problems if + // current MAC is different :( So we use the local MAC instead, without writing it into the game's memory. + SceNetEtherAddr localSrcMac; if (saddr != NULL) { - getLocalMac(saddr); + getLocalMac(&localSrcMac); + saddr = &localSrcMac; } // Valid Addresses. FIXME: MAC only valid after successful attempt to Create/Connect/Join a Group? (ie. adhocctlCurrentMode != ADHOCCTL_MODE_NONE) // The broadcast address is accepted, as on a PSP (pspautotests net/adhoc/ptp); connecting to it just never succeeds. @@ -5683,9 +5689,12 @@ static int sceNetAdhocPtpListen(MIPSState *mips, const char *srcmac, int sport, } if (netAdhocInited) { - // Some games (ie. DBZ Shin Budokai 2) might be getting the saddr/srcmac content from SaveState and causing problems :( So we try to fix it here + // Some games (ie. DBZ Shin Budokai 2) might be getting the saddr/srcmac content from SaveState and causing problems if + // current MAC is different :( So we use the local MAC instead, without writing it into the game's memory. + SceNetEtherAddr localSrcMac; if (saddr != NULL) { - getLocalMac(saddr); + getLocalMac(&localSrcMac); + saddr = &localSrcMac; } // Valid Address. FIXME: MAC only valid after successful attempt to Create/Connect/Join a Group? (ie. adhocctlCurrentMode != ADHOCCTL_MODE_NONE) if ((adhocctlCurrentMode != ADHOCCTL_MODE_NONE) && saddr != NULL && isLocalMAC(saddr)) { From 603a63bc090e2d1c0bebea8175efc95ea7bd2038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 20:18:19 +0100 Subject: [PATCH 12/24] sceNetAdhocMatching: Report the pool as empty right after Init A PSP reports the pool size less 0x20 and a maximum of 0 until anything is allocated (net/adhoc/matching, recorded on a PSP). With a context the faked half-pool maximum stays, since that isn't measured. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceNetAdhocMatching.cpp | 22 ++++++++++++++++------ pspautotests | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Core/HLE/sceNetAdhocMatching.cpp b/Core/HLE/sceNetAdhocMatching.cpp index 00a8f46108..df9d5c9188 100644 --- a/Core/HLE/sceNetAdhocMatching.cpp +++ b/Core/HLE/sceNetAdhocMatching.cpp @@ -2666,14 +2666,24 @@ int sceNetAdhocMatchingAbortSendData(int matchingId, const char *mac) { return hleLogDebug(Log::sceNet, 0); } +// A PSP keeps 0x20 bytes of the pool for itself. Its maximum is the largest allocation so far: 0 right +// after Init (pspautotests net/adhoc/matching). Once there's a context, half the pool, which +// isn't measured. +static u32 FakePoolSize() { + return fakePoolSize >= 0x20 ? fakePoolSize - 0x20 : 0; +} + +static u32 FakePoolMaxAlloc() { + return contexts != NULL ? FakePoolSize() / 2 : 0; +} + // Get the maximum memory usage by the matching library static int sceNetAdhocMatchingGetPoolMaxAlloc() { if (!g_Config.bEnableWlan) { return hleLogError(Log::sceNet, -1, "WLAN off"); } - // Lazy way out - hardcoded return value - return hleLogDebug(Log::sceNet, fakePoolSize / 2, "faked value"); + return hleLogDebug(Log::sceNet, FakePoolMaxAlloc(), "faked value"); } int sceNetAdhocMatchingGetPoolStat(u32 poolstatPtr) { @@ -2694,10 +2704,10 @@ int sceNetAdhocMatchingGetPoolStat(u32 poolstatPtr) { return hleLogError(Log::sceNet, SCE_NET_ADHOC_MATCHING_ERROR_INVALID_ARG, "adhocmatching invalid arg"); } - // Fill Poolstat with Fake Data - poolstat->pool = fakePoolSize; - poolstat->maximum = fakePoolSize / 2; // Max usage faked to halt the pool - poolstat->free = fakePoolSize - poolstat->maximum; + // Fake data, apart from right after Init. + poolstat->pool = FakePoolSize(); + poolstat->maximum = FakePoolMaxAlloc(); + poolstat->free = poolstat->pool - poolstat->maximum; // Return Success return hleLogDebug(Log::sceNet, 0); diff --git a/pspautotests b/pspautotests index 212f5b3f57..f979cad6c3 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit 212f5b3f5776aa770add6cb96a5d2bf5973f6770 +Subproject commit f979cad6c332e0c59084d46e47093a8d405ebf52 From 8912d956ffb563b69b615c8f54ce907e9b1a0455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 20:28:21 +0100 Subject: [PATCH 13/24] test.py: Run the passing single-console ad hoc tests with the good ones alerts, discover, gamemode, limits, loopback, self, socketids and term pass, so they move to tests_good. They run in real time against the built-in server, about 71 s together. The ad hoc tests left in tests_next say why they don't pass yet. Co-Authored-By: Claude Opus 5.5 --- test.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/test.py b/test.py index 7afea4dfb4..492a677347 100755 --- a/test.py +++ b/test.py @@ -363,6 +363,14 @@ tests_good = [ "net/inet/polledge", "net/inet/reuse", "net/inet/errnothread", + "net/adhoc/alerts", + "net/adhoc/discover", + "net/adhoc/gamemode", + "net/adhoc/limits", + "net/adhoc/loopback", + "net/adhoc/self", + "net/adhoc/socketids", + "net/adhoc/term", "misc/libc", "misc/sdkver", "misc/testgp", @@ -668,20 +676,12 @@ tests_next = [ #"modules/loadexec/loader", "modules/unloadfill/unloadfill", "net/http/http", - "net/adhoc/adhocctl", - "net/adhoc/alerts", - "net/adhoc/ctlinit", - "net/adhoc/discover", - "net/adhoc/gamemode", - "net/adhoc/limits", - "net/adhoc/loopback", - "net/adhoc/matching", - "net/adhoc/pdp", - "net/adhoc/poll", - "net/adhoc/ptp", - "net/adhoc/self", - "net/adhoc/socketids", - "net/adhoc/term", + "net/adhoc/adhocctl", # a second Init fails and leaves adhocctl uninitialized; also the Scan state race below + "net/adhoc/ctlinit", # GetState right after Scan: 2 here, still 0 on a PSP for under 1 ms (net/adhoc/scanstate) + "net/adhoc/matching", # GetHelloOpt reports the hello length + 16; what the 16 bytes are is unknown + "net/adhoc/pdp", # PdpCreate takes any MAC as the local one, for savestates made with another MAC + "net/adhoc/poll", # P2P: a peer's close and a refused connect aren't seen in time + "net/adhoc/ptp", # the PdpCreate MAC as in pdp; a self-connect is established at open; refused connects in P2P "net/inet/bind", "net/inet/poll", "net/inet/errors", From c67315779b1a5ac7ed263578c16786b4cb7c064d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 20:41:20 +0100 Subject: [PATCH 14/24] sceNetAdhocctlInit: Log loudly when called twice A PSP is left uninitialized by it (net/adhoc/adhocctl); not emulated. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceNetAdhoc.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 1cac4bf46a..08f121f451 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -2397,6 +2397,10 @@ int sceNetAdhocctlInit(MIPSState *mips, int stackSize, int prio, u32 productAddr // FIXME: Sometimes returning 0x80410601 (SCE_NET_ADHOC_ERROR_AUTH_ALREADY_INITIALIZED / Library module is already initialized ?) when AdhocctlTerm is not fully done? if (netAdhocctlInited) { + // On a PSP this also leaves adhocctl uninitialized (GetState fails with NOT_INITIALIZED), and + // a further Init fails with AUTH_ALREADY_INITIALIZED until Term (pspautotests net/adhoc/adhocctl). + // Not emulated: a game doing this would lose its connection, and none has been seen to. + ERROR_LOG_REPORT(Log::sceNet, "sceNetAdhocctlInit called while already initialized - a PSP would now be left uninitialized, which isn't emulated"); return hleLogError(Log::sceNet, SCE_NET_ADHOCCTL_ERROR_ALREADY_INITIALIZED); } From eb254d86e875bcd51e274f551e5761f57b271884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 20:51:47 +0100 Subject: [PATCH 15/24] sceNetAdhoc: Don't take a refused PTP connect for an established one On macOS, calling connect() again on a socket whose non-blocking connect was refused returns EISCONN; only SO_ERROR and a failing getpeername tell it apart from a connection. PtpConnect took EISCONN at its word, so in P2P mode a connect to a port nobody listened on "succeeded", and the game sent into nothing. Check getpeername first and use SO_ERROR when it fails. PollSocket also reports a refused connect as DISCONNECT now, as a PSP does (net/adhoc/poll), instead of only ever noticing success. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceNetAdhoc.cpp | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 08f121f451..31a15b3c09 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -878,6 +878,22 @@ static void ptpSetEstablished(AdhocSocket *sock) { // A connection in progress shows up as established in GetPtpStat, and PollSocket reports CONNECT, as // soon as it's up (pspautotests net/adhoc/pair/ptpconnect). +// Whether a socket whose connect() returned EISCONN is really connected. macOS answers a repeated +// connect() with EISCONN after the first one was refused too; then getpeername fails and the refusal +// is in SO_ERROR, which goes to *error. +static bool ptpReallyConnected(int fd, int *error) { + struct sockaddr_in sin; + socklen_t sinlen = sizeof(sin); + if (getpeername(fd, (struct sockaddr *)&sin, &sinlen) == 0) { + return true; + } + int soError = 0; + socklen_t soErrorLen = sizeof(soError); + getsockopt(fd, SOL_SOCKET, SO_ERROR, (char *)&soError, &soErrorLen); + *error = soError != 0 ? soError : ECONNREFUSED; + return false; +} + static void ptpUpdateConnecting(AdhocSocket *sock) { if (sock->data.ptp.state != ADHOC_PTP_STATE_SYN_SENT && sock->data.ptp.state != ADHOC_PTP_STATE_SYN_RCVD) { return; @@ -1719,6 +1735,9 @@ int DoBlockingPtpConnect(AdhocSocketRequest& req, s64& result, AdhocSendTargets& } else { ret = connect(ptpsocket.id, (struct sockaddr*)&sin, sizeof(sin)); sockerr = socket_errno; + if (ret == SOCKET_ERROR && sockerr == EISCONN) { + ptpReallyConnected(ptpsocket.id, &sockerr); + } } if (sockerr != 0) DEBUG_LOG(Log::sceNet, "sceNetAdhocPtpConnect[%i:%u]: connect(%i) error = %i", req.id, ptpsocket.lport, ptpsocket.id, sockerr); @@ -3524,11 +3543,16 @@ int PollAdhocSocket(SceNetAdhocPollSd* sds, int count, int timeout, int nonblock // to take. A connection whose peer has closed reports DISCONNECT alone. const bool readable = serverHasRelay ? postoffice_can_recv_or_listen_has_request(sds[i].id - 1) : FD_ISSET(fd, &readfds); // A connection PtpOpen started may have come up since anything looked at it. - if (sock->type == SOCK_PTP && sock->data.ptp.state == ADHOC_PTP_STATE_SYN_SENT && !serverHasRelay && FD_ISSET(fd, &writefds)) { - struct sockaddr_in peer; - socklen_t peerLen = sizeof(peer); - if (getpeername(fd, (struct sockaddr *)&peer, &peerLen) == 0) { + // A refused one reports DISCONNECT (pspautotests net/adhoc/poll). + if (sock->type == SOCK_PTP && sock->data.ptp.state == ADHOC_PTP_STATE_SYN_SENT && !serverHasRelay && (FD_ISSET(fd, &writefds) || FD_ISSET(fd, &exceptfds))) { + int connectError = 0; + if (ptpReallyConnected(fd, &connectError)) { ptpSetEstablished(sock); + } else if (!connectInProgress(connectError)) { + if (RecreatePtpSocket(sds[i].id) < 0) { + WARN_LOG(Log::sceNet, "sceNetAdhocPollSocket[%i]: Failed to recreate the refused socket", sds[i].id); + } + sock->data.ptp.state = ADHOC_PTP_STATE_CLOSED; } } if (sock->type == SOCK_PTP && sock->data.ptp.state == ADHOC_PTP_STATE_LISTEN) { @@ -5430,6 +5454,9 @@ int NetAdhocPtp_Connect(int id, int timeout, int flag, bool allowForcedConnect) } else { connectresult = connect(ptpsocket.id, (struct sockaddr*)&sin, sizeof(sin)); errorcode = socket_errno; + if (connectresult == SOCKET_ERROR && errorcode == EISCONN) { + ptpReallyConnected(ptpsocket.id, &errorcode); + } } if (connectresult == SOCKET_ERROR) { From b707ff4306fb3823ee7d3208142fffa5b150306b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 20:52:03 +0100 Subject: [PATCH 16/24] test.py: Update the reasons for poll and ptp Co-Authored-By: Claude Opus 5.5 --- test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test.py b/test.py index 492a677347..198f24eb5c 100755 --- a/test.py +++ b/test.py @@ -680,8 +680,8 @@ tests_next = [ "net/adhoc/ctlinit", # GetState right after Scan: 2 here, still 0 on a PSP for under 1 ms (net/adhoc/scanstate) "net/adhoc/matching", # GetHelloOpt reports the hello length + 16; what the 16 bytes are is unknown "net/adhoc/pdp", # PdpCreate takes any MAC as the local one, for savestates made with another MAC - "net/adhoc/poll", # P2P: a peer's close and a refused connect aren't seen in time - "net/adhoc/ptp", # the PdpCreate MAC as in pdp; a self-connect is established at open; refused connects in P2P + "net/adhoc/poll", # P2P: a peer's close isn't seen while its data is still unread + "net/adhoc/ptp", # the PdpCreate MAC as in pdp; a self-connect is established at open; a refused connect retries until timeout instead of CONNECTION_REFUSED "net/inet/bind", "net/inet/poll", "net/inet/errors", From e7548e1bea018c6209601b43c843b5cc5a30213c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 22:23:09 +0100 Subject: [PATCH 17/24] sceNetAdhoc: Measure socket and game mode timeouts in emulated time The timeouts of blocking socket calls and of entering game mode are the PSP's, but they were measured on the wall clock, so they came out shorter in PSP time whenever emulation ran fast or slow. The wait for the ad hoc server, which is a host-side round trip, stays on the wall clock. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/proAdhoc.cpp | 3 ++- Core/HLE/proAdhoc.h | 2 ++ Core/HLE/sceNetAdhoc.cpp | 25 +++++++++++++++---------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Core/HLE/proAdhoc.cpp b/Core/HLE/proAdhoc.cpp index f94f599465..07b7384163 100644 --- a/Core/HLE/proAdhoc.cpp +++ b/Core/HLE/proAdhoc.cpp @@ -85,6 +85,7 @@ SceNetAdhocctlPeerInfo * friends = NULL; SceNetAdhocctlScanInfo * networks = NULL; SceNetAdhocctlScanInfo * newnetworks = NULL; u64 adhocctlStartTime = 0; +u64 gameModeEnterTimeUs = 0; std::atomic isAdhocctlNeedLogin = false; bool isAdhocctlBusy = false; int adhocctlState = ADHOCCTL_STATE_DISCONNECTED; @@ -1641,7 +1642,7 @@ void friendFinderPoll() { uint64_t now = time_now_d() * 1000000.0; // Use time_now_d()*1000000.0 instead of CoreTiming::GetGlobalTimeUsScaled() if the game gets disconnected from AdhocServer too soon when FPS wasn't stable // Calculate EnterGameMode Timeout to prevent waiting forever for disconnected players - if (isAdhocctlBusy && adhocctlState == ADHOCCTL_STATE_DISCONNECTED && adhocctlCurrentMode == ADHOCCTL_MODE_GAMEMODE && netAdhocGameModeEntered && static_cast(now - adhocctlStartTime) > netAdhocEnterGameModeTimeout) { + if (isAdhocctlBusy && adhocctlState == ADHOCCTL_STATE_DISCONNECTED && adhocctlCurrentMode == ADHOCCTL_MODE_GAMEMODE && netAdhocGameModeEntered && static_cast(CoreTiming::GetGlobalTimeUs() - gameModeEnterTimeUs) > netAdhocEnterGameModeTimeout) { netAdhocGameModeEntered = false; // A joiner's GAMEMODE is queued as soon as the server answers, and held until the host shows up. // It must not come after all (pspautotests net/adhoc/gamemode). diff --git a/Core/HLE/proAdhoc.h b/Core/HLE/proAdhoc.h index 617eb75f78..3ec740895a 100644 --- a/Core/HLE/proAdhoc.h +++ b/Core/HLE/proAdhoc.h @@ -818,6 +818,8 @@ extern std::atomic friendFinderRunning; extern SceNetAdhocctlPeerInfo * friends; extern SceNetAdhocctlScanInfo * networks; extern u64 adhocctlStartTime; +// When CreateEnterGameMode or JoinEnterGameMode was called, in emulated time: its timeout is a PSP-side one. +extern u64 gameModeEnterTimeUs; extern std::atomic isAdhocctlNeedLogin; extern bool isAdhocctlBusy; extern int adhocctlState; diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 31a15b3c09..7bbeee8b00 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -1053,7 +1053,7 @@ int DoBlockingPdpRecv(AdhocSocketRequest& req, s64& result) { memset(&sin, 0, sinlen); recvfrom(pdpsocket.id, dummyPeekBuf64k, dummyPeekBuf64kSize, MSG_NOSIGNAL, (struct sockaddr*)&sin, &sinlen); // Try again later, until timeout reached - u64 now = (u64)(time_now_d() * 1000000.0); + u64 now = CoreTiming::GetGlobalTimeUs(); if (req.timeout != 0 && now - req.startTime > req.timeout) { result = SCE_NET_ADHOC_ERROR_TIMEOUT; DEBUG_LOG(Log::sceNet, "sceNetAdhocPdpRecv[%i]: Discard Timeout", req.id); @@ -1103,7 +1103,7 @@ int DoBlockingPdpRecv(AdhocSocketRequest& req, s64& result) { } // On Windows: recvfrom on UDP can get error WSAECONNRESET when previous sendto's destination is unreachable (or destination port is not bound yet), may need to disable SIO_UDP_CONNRESET error else if (sockerr == EAGAIN || sockerr == EWOULDBLOCK || sockerr == ECONNRESET) { - u64 now = (u64)(time_now_d() * 1000000.0); + u64 now = CoreTiming::GetGlobalTimeUs(); if (req.timeout == 0 || now - req.startTime <= req.timeout) { // Try again later return -1; @@ -1215,7 +1215,7 @@ int DoBlockingPdpSend(AdhocSocketRequest& req, s64& result, AdhocSendTargets& ta } else { if (ret == SOCKET_ERROR && (sockerr == EAGAIN || sockerr == EWOULDBLOCK)) { - u64 now = (u64)(time_now_d() * 1000000.0); + u64 now = CoreTiming::GetGlobalTimeUs(); if (req.timeout == 0 || now - req.startTime <= req.timeout) { retry = true; } @@ -1340,7 +1340,7 @@ int DoBlockingPtpSend(AdhocSocketRequest& req, s64& result) { result = 0; } else if (ret == SOCKET_ERROR && (sockerr == EAGAIN || sockerr == EWOULDBLOCK || (ptpsocket.state == ADHOC_PTP_STATE_SYN_SENT && (sockerr == ENOTCONN || connectInProgress(sockerr))))) { - u64 now = (u64)(time_now_d() * 1000000.0); + u64 now = CoreTiming::GetGlobalTimeUs(); if (req.timeout == 0 || now - req.startTime <= req.timeout) { return -1; } @@ -1456,7 +1456,7 @@ int DoBlockingPtpRecv(AdhocSocketRequest& req, s64& result) { result = 0; } else if (ret == SOCKET_ERROR && (sockerr == EAGAIN || sockerr == EWOULDBLOCK || (ptpsocket.state == ADHOC_PTP_STATE_SYN_SENT && (sockerr == ENOTCONN || connectInProgress(sockerr))))) { - u64 now = (u64)(time_now_d() * 1000000.0); + u64 now = CoreTiming::GetGlobalTimeUs(); if (req.timeout == 0 || now - req.startTime <= req.timeout) { return -1; } @@ -1632,7 +1632,7 @@ int DoBlockingPtpAccept(AdhocSocketRequest& req, s64& result) { result = newid; } else if (ret == 0 || (ret == SOCKET_ERROR && (sockerr == EAGAIN || sockerr == EWOULDBLOCK))) { - u64 now = (u64)(time_now_d() * 1000000.0); + u64 now = CoreTiming::GetGlobalTimeUs(); if (req.timeout == 0 || now - req.startTime <= req.timeout) { return -1; } @@ -1814,7 +1814,7 @@ int DoBlockingPtpConnect(AdhocSocketRequest& req, s64& result, AdhocSendTargets& // Still in progress, try again next time until Timedout if (ptpsocket.state != ADHOC_PTP_STATE_ESTABLISHED) { - u64 now = (u64)(time_now_d() * 1000000.0); + u64 now = CoreTiming::GetGlobalTimeUs(); if (req.timeout == 0 || now - req.startTime <= req.timeout) { // Try again later return -1; @@ -1855,7 +1855,7 @@ int DoBlockingPtpFlush(AdhocSocketRequest& req, s64& result) { result = 0; if (sockerr == EAGAIN || sockerr == EWOULDBLOCK) { - u64 now = (u64)(time_now_d() * 1000000.0); + u64 now = CoreTiming::GetGlobalTimeUs(); if (req.timeout == 0 || now - req.startTime <= req.timeout) { return -1; } @@ -1877,7 +1877,7 @@ int DoBlockingAdhocPollSocket(AdhocSocketRequest& req, s64& result) { // A disconnected socket or a poll alert ends the wait at once. ret = SCE_NET_ADHOC_ERROR_EXCEPTION_EVENT; } else if (ret == 0) { - u64 now = (u64)(time_now_d() * 1000000.0); + u64 now = CoreTiming::GetGlobalTimeUs(); // A timeout of 0 waits forever, like the rest of sceNet. if (static_cast(req.timeout) <= 0 || now - req.startTime <= req.timeout) { return -1; @@ -2019,7 +2019,8 @@ int WaitBlockingAdhocSocket(u64 threadSocketId, int type, int pspSocketId, void* if (tmout > 0) tmout = std::max(tmout, minSocketTimeoutUS); - u64 startTime = (u64)(time_now_d() * 1000000.0); + // Timeouts are the PSP's, so they run on the emulated clock. + u64 startTime = CoreTiming::GetGlobalTimeUs(); adhocSocketRequests[threadSocketId] = { type, pspSocketId, buffer, len, tmout, startTime, remoteMAC, remotePort }; // Some games (ie. Hitman Reborn Battle Arena 2) are using as small as 50 usec timeout CoreTiming::ScheduleEvent(usToCycles(1), adhocSocketNotifyEvent, threadSocketId); @@ -2179,6 +2180,8 @@ void __NetAdhocDoState(PointerWrap &p) { Do(p, isAdhocctlBusy); Do(p, netAdhocGameModeEntered); Do(p, netAdhocEnterGameModeTimeout); + // Not saved: a game mode being entered gets its whole timeout again after a load. + gameModeEnterTimeUs = CoreTiming::GetGlobalTimeUs(); } else { isAdhocctlBusy = false; @@ -4447,6 +4450,7 @@ int NetAdhocctl_CreateEnterGameMode(const char* group_name, int game_type, int n adhocConnectionType = ADHOC_CREATE; netAdhocGameModeEntered = true; netAdhocEnterGameModeTimeout = timeout; + gameModeEnterTimeUs = CoreTiming::GetGlobalTimeUs(); return NetAdhocctl_Create(group_name); } @@ -4509,6 +4513,7 @@ static int sceNetAdhocctlJoinEnterGameMode(MIPSState *mips, const char * group_n adhocConnectionType = ADHOC_JOIN; netAdhocGameModeEntered = true; netAdhocEnterGameModeTimeout = timeout; + gameModeEnterTimeUs = CoreTiming::GetGlobalTimeUs(); return hleLogDebug(Log::sceNet, NetAdhocctl_Create(group_name)); } From c52f8c96577f6ccf91766cdcafcfb7186a73dd8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 22:23:09 +0100 Subject: [PATCH 18/24] Headless: --speed and --port-offset, and run the ad hoc tests at 8x --speed=PERCENT runs a --realtime headless at an alternate speed (speed control while connected is allowed for it), and pacing follows it. The ad hoc tests now run at 800%, which takes their part of test.py -g from about 71 s to 12 s; only local network round trips stay real time. test.py also gives each ad hoc test run its own --port-offset: a connection the previous run made with the same ports is still in TCP TIME_WAIT, and connecting again failed with EADDRINUSE once runs came close together. Co-Authored-By: Claude Opus 5.5 --- Core/CmdLine.cpp | 15 +++++++++++++++ Core/CmdLine.h | 5 +++++ headless/Headless.cpp | 9 ++++++++- test.py | 10 ++++++++-- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index 7ea12e14a1..947a89cddb 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -256,6 +256,8 @@ static const CommandLineParam g_autoParams[] = { {POFF(timeoutWall), CmdParamType::Double, "timeout", '\0', "Alias for --timeout-wall", CmdLineMode::Headless}, {POFF(instance), CmdParamType::Int, "instance", '\0', "Run as instance N of several talking to each other over ad hoc (own 127.0.0.N address and MAC)", CmdLineMode::Headless}, {POFF(realtime), CmdParamType::Bool, "realtime", '\0', "Run at PSP speed instead of as fast as possible", CmdLineMode::Headless}, + {POFF(portOffset), CmdParamType::Int, "port-offset", '\0', "Add this to every ad hoc port on the host (default from the config, normally 10000)"}, + {POFF(speedPercent), CmdParamType::Int, "speed", '\0', "With --realtime, run at this percentage of PSP speed (alternate speed 1)", CmdLineMode::Headless}, {POFF(adhocServer), CmdParamType::Bool, "adhoc-server", '\0', "Run the built-in ad hoc server"}, {POFF(nickname), CmdParamType::String, "nickname", '\0', "The PSP's nickname (System settings), for this run only"}, {POFF(lagSync), CmdParamType::Bool, "lag-sync", '\0', "Keep emulated time in step with the wall clock every millisecond, not every frame (the Force real clock sync setting)"}, @@ -663,6 +665,19 @@ void CommandLineOptions::ApplyToConfig() const { g_logManager.DoNotSaveChannels(); } + if (portOffset.has_value()) { + g_Config.iPortOffset = portOffset.value(); + g_Config.DoNotSaveSetting(&g_Config.iPortOffset); + } + + if (speedPercent.has_value()) { + g_Config.iAltSpeedPercent1 = speedPercent.value(); + g_Config.DoNotSaveSetting(&g_Config.iAltSpeedPercent1); + // Ad hoc tests are what this is for, and speed control is otherwise off while connected. + g_Config.bAllowSpeedControlWhileConnected = true; + g_Config.DoNotSaveSetting(&g_Config.bAllowSpeedControlWhileConnected); + } + if (lockedCPUSpeed.has_value()) { g_Config.iLockedCPUSpeed = lockedCPUSpeed.value(); g_Config.DoNotSaveSetting(&g_Config.iLockedCPUSpeed); diff --git a/Core/CmdLine.h b/Core/CmdLine.h index cfb79b2be8..0ee8091eeb 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -199,6 +199,11 @@ struct CommandLineOptions { // Both are for ad hoc tests where instances talk to each other. std::optional instance; std::optional realtime; + // Headless with --realtime: run at this percentage of PSP speed, through alternate speed 1. + std::optional speedPercent; + // Added to every ad hoc port on the host. Back-to-back test runs vary it, so a connection doesn't + // collide with the previous run's, still in TCP TIME_WAIT. + std::optional portOffset; std::optional adhocServer; std::optional lagSync; std::optional nickname; diff --git a/headless/Headless.cpp b/headless/Headless.cpp index 5f4fd0ed4e..a05d52cde3 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -63,6 +63,7 @@ #include "Core/HLE/HLE.h" #include "Core/HLE/__sceAudio.h" #include "Core/HLE/sceCtrl.h" +#include "Core/HLE/sceDisplay.h" #include "Core/MIPS/MIPSTables.h" #include "Core/System.h" #include "Core/Util/PSARUnpack.h" @@ -665,7 +666,10 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar // draw never flips, so the emulated clock would race ahead of instances it talks to. The JIT // cores only return at the end of a frame, so like the app, this paces once per frame. blockTicks = (int)usToCycles(1000); - const double ahead = emulatedElapsed - (time_now_d() - startTime); + // At an alternate speed (--speed), the emulated clock runs that much faster than the wall clock. + const int frameLimit = __DisplayGetFrameTimingLimit(); + const double speed = frameLimit > 0 ? frameLimit / 60.0 : 1.0; + const double ahead = (emulatedElapsed - (time_now_d() - startTime) * speed) / speed; // With --lag-sync, lag sync paces every emulated millisecond instead, and two pacers out of // phase would undo each other. if (ahead > 0.001 && !g_Config.bForceLagSync) { @@ -1345,6 +1349,9 @@ int main(int argc, const char* argv[]) { coreParameter.pixelHeight = 272 * coreParameter.renderScaleFactor; // Ad hoc tests between instances wait for each other over the real network, so they need PSP speed. coreParameter.fastForward = !cmdLineOptions.realtime.value_or(false); + if (cmdLineOptions.speedPercent.has_value()) { + coreParameter.fpsLimit = FPSLimit::CUSTOM1; + } #if PPSSPP_PLATFORM(IOS) // GetExeDirectory() knows nothing on iOS, but argv[0] is the executable inside the .app, diff --git a/test.py b/test.py index 198f24eb5c..47abe0a2ca 100755 --- a/test.py +++ b/test.py @@ -5,6 +5,7 @@ import sys import os import subprocess +import random import threading import glob import platform @@ -747,13 +748,17 @@ test_flags = { # The ad hoc tests create groups, so they need a server to log in to, and they wait for its replies # in real time. -ADHOC_FLAGS = ["--adhoc-server", "--realtime", "--timeout-wall=60"] +# They run at 8x PSP speed: timeouts and delays are in emulated time, and only the local network round +# trips stay real. +ADHOC_FLAGS = ["--adhoc-server", "--realtime", "--speed=" + os.environ.get("ADHOC_SPEED", "800"), "--timeout-wall=60"] def flags_for(test): if test in test_flags: return test_flags[test] if test.startswith("net/adhoc/"): - return ADHOC_FLAGS + # A new port offset each run: a connection the last run made with the same ports is still in TCP + # TIME_WAIT for a while, and connecting again fails. + return ADHOC_FLAGS + ["--port-offset=%d" % random.randrange(10000, 30000)] return None def is_pair_test(test): @@ -846,6 +851,7 @@ def run_pair_tests(test_list, args): # built-in ad hoc server and relay. cmdline = [sys.executable, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'Tools', 'adhoc_multi.py'), '--headless', PPSSPP_EXE, '--relay'] cmdline.extend([i for i in args if i not in ['-g', '-m', '-b', '--teamcity']]) + cmdline.append("--speed=" + os.environ.get("ADHOC_SPEED", "800")) cmdline.extend(test_list) returncode = subprocess.call(cmdline) print("Ran " + ' '.join(cmdline)) From 043875a607bbeb5a46f2ca4be040641f18c1badd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 22:34:10 +0100 Subject: [PATCH 19/24] pspautotests: matching2 and matchingorder no longer depend on timing Co-Authored-By: Claude Opus 5.5 --- pspautotests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pspautotests b/pspautotests index f979cad6c3..f573fe8c91 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit f979cad6c332e0c59084d46e47093a8d405ebf52 +Subproject commit f573fe8c9122a8ff5db4f030d74557bed644b4e0 From 9d8ef5e8f5a60296ee29ae53e8e4f460ea79986f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 30 Oct 2026 11:21:57 +0100 Subject: [PATCH 20/24] sceNetAdhoc: Ignore the server's answer to a timed-out game mode join When entering game mode timed out before the server's group BSSID came back, the late BSSID was taken for a normal group's, and the state went to CONNECTED: every later CreateEnterGameMode then got ALREADY_CONNECTED. At the ad hoc tests' 8x speed the 1 s timeouts in net/adhoc/gamemode are 125 ms of wall time, short enough for that on Windows; at 50x it happens on macOS too. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/proAdhoc.cpp | 7 +++++++ Core/HLE/proAdhoc.h | 3 +++ Core/HLE/sceNetAdhoc.cpp | 3 +++ 3 files changed, 13 insertions(+) diff --git a/Core/HLE/proAdhoc.cpp b/Core/HLE/proAdhoc.cpp index 07b7384163..66eb0c250d 100644 --- a/Core/HLE/proAdhoc.cpp +++ b/Core/HLE/proAdhoc.cpp @@ -86,6 +86,7 @@ SceNetAdhocctlScanInfo * networks = NULL; SceNetAdhocctlScanInfo * newnetworks = NULL; u64 adhocctlStartTime = 0; u64 gameModeEnterTimeUs = 0; +bool dropLateGroupReply = false; std::atomic isAdhocctlNeedLogin = false; bool isAdhocctlBusy = false; int adhocctlState = ADHOCCTL_STATE_DISCONNECTED; @@ -1696,6 +1697,12 @@ void friendFinderPoll() { break; case ADHOCCTL_CLIENT_EVENT_LEADER_MAC:{ INFO_LOG(Log::sceNet, "FriendFinder: Incoming OPCODE_CONNECT_BSSID [%s]", mac2str((SceNetEtherAddr *)adhocctl_event.leader_mac.mac).c_str()); + if (dropLateGroupReply) { + // The game mode it was for has timed out (we've already left the group again). + dropLateGroupReply = false; + INFO_LOG(Log::sceNet, "FriendFinder: Ignoring the group BSSID, game mode timed out before it came"); + break; + } // Update Group BSSID memcpy(parameter.bssid.mac_addr.data, adhocctl_event.leader_mac.mac, 6); // This packet seems to contains Adhoc Group Creator's BSSID (similar to AP's BSSID) so it shouldn't get mixed up with local MAC address. Note: On JPCSP + prx files params.bssid is hardcoded to "Jpcsp\0" and doesn't match to any of player's mac diff --git a/Core/HLE/proAdhoc.h b/Core/HLE/proAdhoc.h index 3ec740895a..77e57b995a 100644 --- a/Core/HLE/proAdhoc.h +++ b/Core/HLE/proAdhoc.h @@ -820,6 +820,9 @@ extern SceNetAdhocctlScanInfo * networks; extern u64 adhocctlStartTime; // When CreateEnterGameMode or JoinEnterGameMode was called, in emulated time: its timeout is a PSP-side one. extern u64 gameModeEnterTimeUs; +// Set when entering game mode timed out, until the next group join is sent: the server's answer to the +// timed-out join may still be on its way, and must not connect us to the group. +extern bool dropLateGroupReply; extern std::atomic isAdhocctlNeedLogin; extern bool isAdhocctlBusy; extern int adhocctlState; diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 7bbeee8b00..523976cbe1 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -626,6 +626,7 @@ static void __AdhocctlNotify(u64 userdata, int cyclesLate) { call_status = adhocctl_connect(adhocctlHandle, (char *)req.group.data); if (call_status == ADHOCCTL_CALL_SUCCESS) { adhocctlInServerGroup = true; + dropLateGroupReply = false; } break; } @@ -2271,6 +2272,8 @@ void __UpdateAdhocctlHandlers(u32 flag, u32 error) { } void NetAdhocctl_GameModeTimedOut() { + // If the server's BSSID came in time, there's nothing to drop, and the next join clears this. + dropLateGroupReply = true; if (adhocctlInServerGroup) { const std::lock_guard adhocctl_guard(adhocctlMutex); if (adhocctlHandle != NULL) { From a0669ffbe497321b25dcd62a228d6ee155500f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 30 Oct 2026 11:53:44 +0100 Subject: [PATCH 21/24] sceNetAdhoc: Don't let an abandoned group join put us in the group When the game gives up on a join (game mode timed out, or it disconnected), a join still queued isn't sent anymore, and the rest of the server's answer to one already sent is dropped: its players, and the BSSID that came last and connected us. Players joining a group we've just left (before the server knew) are dropped too, instead of staying in our peer list. Replaces the previous fix, which only dropped a late BSSID: on Windows the join itself was still queued when game mode timed out in net/adhoc/gamemode, and went out after it. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/proAdhoc.cpp | 19 ++++++++++++++----- Core/HLE/proAdhoc.h | 13 ++++++++++--- Core/HLE/sceNetAdhoc.cpp | 24 +++++++++++++++++++++--- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/Core/HLE/proAdhoc.cpp b/Core/HLE/proAdhoc.cpp index 66eb0c250d..ebdc1192e5 100644 --- a/Core/HLE/proAdhoc.cpp +++ b/Core/HLE/proAdhoc.cpp @@ -86,7 +86,8 @@ SceNetAdhocctlScanInfo * networks = NULL; SceNetAdhocctlScanInfo * newnetworks = NULL; u64 adhocctlStartTime = 0; u64 gameModeEnterTimeUs = 0; -bool dropLateGroupReply = false; +bool groupJoinInFlight = false; +int abandonedGroupJoins = 0; std::atomic isAdhocctlNeedLogin = false; bool isAdhocctlBusy = false; int adhocctlState = ADHOCCTL_STATE_DISCONNECTED; @@ -1377,6 +1378,9 @@ void killAdhocctlClient(bool due_to_error, const std::lock_guard &lo destroy_adhocctl_session(adhocctlHandle); adhocctlHandle = NULL; + // Nothing more comes from that session. + groupJoinInFlight = false; + abandonedGroupJoins = 0; // Mark all friends as timedout since we won't be able to detects disconnected friends anymore without being connected to Adhoc Server peerlock.lock(); @@ -1697,12 +1701,12 @@ void friendFinderPoll() { break; case ADHOCCTL_CLIENT_EVENT_LEADER_MAC:{ INFO_LOG(Log::sceNet, "FriendFinder: Incoming OPCODE_CONNECT_BSSID [%s]", mac2str((SceNetEtherAddr *)adhocctl_event.leader_mac.mac).c_str()); - if (dropLateGroupReply) { - // The game mode it was for has timed out (we've already left the group again). - dropLateGroupReply = false; - INFO_LOG(Log::sceNet, "FriendFinder: Ignoring the group BSSID, game mode timed out before it came"); + if (abandonedGroupJoins > 0) { + abandonedGroupJoins--; + INFO_LOG(Log::sceNet, "FriendFinder: Ignoring the BSSID of an abandoned group join"); break; } + groupJoinInFlight = false; // Update Group BSSID memcpy(parameter.bssid.mac_addr.data, adhocctl_event.leader_mac.mac, 6); // This packet seems to contains Adhoc Group Creator's BSSID (similar to AP's BSSID) so it shouldn't get mixed up with local MAC address. Note: On JPCSP + prx files params.bssid is hardcoded to "Jpcsp\0" and doesn't match to any of player's mac @@ -1762,6 +1766,11 @@ void friendFinderPoll() { case ADHOCCTL_CLIENT_EVENT_CONNECT:{ u32_le ipaddr = adhocctl_event.connect.id; INFO_LOG(Log::sceNet, "FriendFinder: Incoming OPCODE_CONNECT [%s][%s][%s]", mac2str((SceNetEtherAddr *)adhocctl_event.connect.mac).c_str(), ip2str(*(in_addr*)&ipaddr).c_str(), adhocctl_event.connect.nickname); + // A player of an abandoned join's group, or of one we've left (the server didn't know yet). + if (abandonedGroupJoins > 0 || !NetAdhocctl_InServerGroup()) { + INFO_LOG(Log::sceNet, "FriendFinder: Ignoring a player of a group we're not joining"); + break; + } // Add User addFriend(adhocctl_event.connect); diff --git a/Core/HLE/proAdhoc.h b/Core/HLE/proAdhoc.h index 77e57b995a..f9ffe2c504 100644 --- a/Core/HLE/proAdhoc.h +++ b/Core/HLE/proAdhoc.h @@ -820,9 +820,16 @@ extern SceNetAdhocctlScanInfo * networks; extern u64 adhocctlStartTime; // When CreateEnterGameMode or JoinEnterGameMode was called, in emulated time: its timeout is a PSP-side one. extern u64 gameModeEnterTimeUs; -// Set when entering game mode timed out, until the next group join is sent: the server's answer to the -// timed-out join may still be on its way, and must not connect us to the group. -extern bool dropLateGroupReply; +// The server answers a group join with a CONNECT for each player already in the group, then the group's +// BSSID. A join the game has given up on (game mode timed out, or it disconnected) before the BSSID came +// is abandoned: what's left of its answer must not put us in the group. +extern bool groupJoinInFlight; +extern int abandonedGroupJoins; +// Gives up on the join being made, if any: one still queued isn't sent, and the answer to one already +// sent is dropped. +void AbandonGroupJoin(); +// Whether we've asked the server to put us in a group, and haven't left it since. +bool NetAdhocctl_InServerGroup(); extern std::atomic isAdhocctlNeedLogin; extern bool isAdhocctlBusy; extern int adhocctlState; diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 523976cbe1..eacd799935 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -626,7 +626,7 @@ static void __AdhocctlNotify(u64 userdata, int cyclesLate) { call_status = adhocctl_connect(adhocctlHandle, (char *)req.group.data); if (call_status == ADHOCCTL_CALL_SUCCESS) { adhocctlInServerGroup = true; - dropLateGroupReply = false; + groupJoinInFlight = true; } break; } @@ -866,6 +866,24 @@ static bool offAdhocNetwork() { return !adhocctlInServerGroup; } +bool NetAdhocctl_InServerGroup() { + return adhocctlInServerGroup; +} + +void AbandonGroupJoin() { + // A queued join completes like a login, sending nothing. (The requests are saved in savestates, so + // it keeps the format.) + for (auto &it : adhocctlRequests) { + if (it.second.opcode == OPCODE_CONNECT) { + it.second.opcode = OPCODE_LOGIN; + } + } + if (groupJoinInFlight) { + groupJoinInFlight = false; + abandonedGroupJoins++; + } +} + static bool isLocalMac(const SceNetEtherAddr *mac) { SceNetEtherAddr localMac; getLocalMac(&localMac); @@ -2272,8 +2290,7 @@ void __UpdateAdhocctlHandlers(u32 flag, u32 error) { } void NetAdhocctl_GameModeTimedOut() { - // If the server's BSSID came in time, there's nothing to drop, and the next join clears this. - dropLateGroupReply = true; + AbandonGroupJoin(); if (adhocctlInServerGroup) { const std::lock_guard adhocctl_guard(adhocctlMutex); if (adhocctlHandle != NULL) { @@ -3996,6 +4013,7 @@ static u32 NetAdhocctl_Disconnect(bool fromGameMode = false) { // or 650 ms after ExitGameMode (net/adhoc/pair/gamemodetiming). adhocctlRequestTimeUs = CoreTiming::GetGlobalTimeUs(); adhocctlDisconnectFromGameMode = fromGameMode; + AbandonGroupJoin(); // Connected State (Adhoc Mode). Attempting to leave a group while not in a group will be kicked out by Adhoc Server (ie. some games tries to disconnect more than once within a short time) if (adhocctlState != ADHOCCTL_STATE_DISCONNECTED || adhocctlInServerGroup) { From d12e19ed35761dc6ae45b8a71b2874f5bb8ef2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 30 Oct 2026 11:53:44 +0100 Subject: [PATCH 22/24] test.py: Keep the ad hoc tests' ports below Linux's ephemeral ports With offsets of up to 30000, a port net/adhoc/limits listened on could be one an outgoing connection was using, and the listen failed. Co-Authored-By: Claude Opus 5.5 --- test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test.py b/test.py index 47abe0a2ca..43ee6abe71 100755 --- a/test.py +++ b/test.py @@ -757,8 +757,9 @@ def flags_for(test): return test_flags[test] if test.startswith("net/adhoc/"): # A new port offset each run: a connection the last run made with the same ports is still in TCP - # TIME_WAIT for a while, and connecting again fails. - return ADHOC_FLAGS + ["--port-offset=%d" % random.randrange(10000, 30000)] + # TIME_WAIT for a while, and connecting again fails. The tests' ports go up to 31000 (game mode's), and + # have to stay below Linux's ephemeral ports (32768 and up), which outgoing connections hold. + return ADHOC_FLAGS + ["--port-offset=%d" % random.randrange(1, 1700)] return None def is_pair_test(test): From c1058fd589ee68f334b919dfe20cee30eb462f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 30 Oct 2026 12:04:33 +0100 Subject: [PATCH 23/24] sceNetAdhoc: Disconnect is BUSY until a Create or Connect has connected As on a PSP (pspautotests net/adhoc/abandon): the Disconnect fails and the join goes on to CONNECT. Before, it was accepted, and the join's late answer from the server could still connect us afterwards. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceNetAdhoc.cpp | 5 +++++ pspautotests | 2 +- test.py | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index eacd799935..cddca16fdb 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -4009,6 +4009,11 @@ static u32 NetAdhocctl_Disconnect(bool fromGameMode = false) { if (isAdhocctlBusy && CoreTiming::IsScheduled(adhocctlNotifyEvent)) { return SCE_NET_ADHOCCTL_ERROR_BUSY; } + // Until a Create or Connect has connected, it's BUSY, and the join goes on (pspautotests + // net/adhoc/abandon). + if (isAdhocctlBusy && adhocctlState == ADHOCCTL_STATE_DISCONNECTED && adhocctlCurrentMode != ADHOCCTL_MODE_NONE) { + return SCE_NET_ADHOCCTL_ERROR_BUSY; + } // Returns at once on a PSP, and DISCONNECT follows after about 105 ms (pspautotests net/adhoc/timing), // or 650 ms after ExitGameMode (net/adhoc/pair/gamemodetiming). adhocctlRequestTimeUs = CoreTiming::GetGlobalTimeUs(); diff --git a/pspautotests b/pspautotests index f573fe8c91..fabe629697 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit f573fe8c9122a8ff5db4f030d74557bed644b4e0 +Subproject commit fabe6296977d20d9c80a54fdcb273c8d2a02054d diff --git a/test.py b/test.py index 43ee6abe71..f9dda25c41 100755 --- a/test.py +++ b/test.py @@ -364,6 +364,7 @@ tests_good = [ "net/inet/polledge", "net/inet/reuse", "net/inet/errnothread", + "net/adhoc/abandon", "net/adhoc/alerts", "net/adhoc/discover", "net/adhoc/gamemode", From 181c90f75812b91345068b37b942a693784a1f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 30 Oct 2026 12:26:52 +0100 Subject: [PATCH 24/24] pspautotests: matchingjoin no longer counts the host's hellos Co-Authored-By: Claude Opus 5.5 --- pspautotests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pspautotests b/pspautotests index fabe629697..bef628fd88 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit fabe6296977d20d9c80a54fdcb273c8d2a02054d +Subproject commit bef628fd88627d3ef206184d696f62537e48985b