From 4197f2326a38a03213c9dc13f5033d1233e4e755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 28 Oct 2026 18:07:42 +0100 Subject: [PATCH 1/3] sceNetAdhoc: A console that leaves the network goes quiet, as on a PSP The vanish pair test (recorded on two PSPs) now passes on both sides, over the relay: - From the Disconnect call, a console's PDP and PTP sends succeed and go nowhere, and nothing reaches it. - A PTP connection to a peer that has left the group fails with DISCONNECTED after rexmt_int * rexmt_cnt. Relay sockets now get their retry settings (and are zeroed), and an accepted one counts as connected. - Matching data to a peer that has left gets DATA_TIMEOUT after the retransmissions instead of a DATA_ACK. - The matching timeout for a silent peer is keepalive_int * init_count, plus MATCHING_TIMEOUT_EXTRA_US (0) to tune for internet latency. It was (keepalive + rexmt) * count + 500ms, later than a PSP's. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/proAdhoc.cpp | 12 ++++++ Core/HLE/proAdhoc.h | 7 ++++ Core/HLE/sceNetAdhoc.cpp | 68 ++++++++++++++++++++++++++++++-- Core/HLE/sceNetAdhocMatching.cpp | 29 ++++++++++++-- 4 files changed, 109 insertions(+), 7 deletions(-) diff --git a/Core/HLE/proAdhoc.cpp b/Core/HLE/proAdhoc.cpp index 543e03a220..b249b35d69 100644 --- a/Core/HLE/proAdhoc.cpp +++ b/Core/HLE/proAdhoc.cpp @@ -255,6 +255,7 @@ void addFriend(const struct adhocctl_event_connect &event) { peer->port_offset = ((isOriPort && !isPrivateIP(peer->ip_addr)) ? 0 : portOffset); // Update TimeStamp peer->last_recv = CoreTiming::GetGlobalTimeUsScaled(); + peer->left_at = 0; } else { // Allocate Structure @@ -535,6 +536,7 @@ void deleteFriendByIP(uint32_t ip) { //peer = NULL; // Instead of removing it from the list we'll make it timed out since most Matching games are moving group and may still need the peer data thus not recognizing it as Unknown peer peer->last_recv = 0; //CoreTiming::GetGlobalTimeUsScaled(); + peer->left_at = CoreTiming::GetGlobalTimeUsScaled(); // Multithreading Unlock peerlock.unlock(); @@ -2362,6 +2364,16 @@ void fixGameMac(SceNetEtherAddr *mac) { } } +u64 peerLeftAt(const SceNetEtherAddr *mac) { + std::lock_guard peer_guard(peerlock); + for (SceNetAdhocctlPeerInfo *peer = friends; peer != NULL; peer = peer->next) { + if (isMacMatch(&peer->mac_addr, mac)) { + return peer->left_at; + } + } + return 0; +} + bool resolveMAC(SceNetEtherAddr* mac, uint32_t* ip, u16* port_offset) { // Get Local MAC Address SceNetEtherAddr localMac; diff --git a/Core/HLE/proAdhoc.h b/Core/HLE/proAdhoc.h index 5cd99ff9c4..09547f2ba0 100644 --- a/Core/HLE/proAdhoc.h +++ b/Core/HLE/proAdhoc.h @@ -228,6 +228,7 @@ typedef struct SceNetAdhocctlPeerInfo { u32_le ip_addr; // internal use only u16_le port_offset; // IP-specific port offset (internal use only) + u64_le left_at; // When the server said it left the group, or 0 (internal use only) } PACK SceNetAdhocctlPeerInfo; // Peer Information with u32 pointers @@ -387,6 +388,9 @@ typedef struct SceNetAdhocMatchingMemberInternal { // The send was aborted with AbortSendData, so its DATA_ACK is discarded. s32_le ackAborted; + + // Data was sent to a peer that has left the group: DATA_TIMEOUT at this time instead of DATA_ACK. + u64_le dataTimeoutAt; } SceNetAdhocMatchingMemberInternal; @@ -1333,6 +1337,9 @@ bool resolveIP(uint32_t ip, SceNetEtherAddr * mac); */ bool resolveMAC(SceNetEtherAddr* mac, uint32_t* ip, u16* port_offset = nullptr); +// When the server said this peer left the group, or 0 if it's there (or unknown). +u64 peerLeftAt(const SceNetEtherAddr *mac); + /** * Fix mac address if the game messed with the first byte * @param mac Peer MAC Address diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 2950809e11..77abd85e77 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -840,6 +840,29 @@ static int pdp_peek_next_size_postoffice(int idx) { return peek_status; } +// Out of any group (from the moment Disconnect is called): a PSP's radio then reaches nobody, and nobody +// reaches it. +static bool offAdhocNetwork() { + return !adhocctlInServerGroup; +} + +static bool isLocalMac(const SceNetEtherAddr *mac) { + SceNetEtherAddr localMac; + getLocalMac(&localMac); + return isMacMatch(&localMac, mac); +} + +// A PTP connection to a peer that has left the group fails once the retransmissions would have run out: +// rexmt_int * rexmt_cnt after it left (pspautotests net/adhoc/pair/vanish). +static bool ptpPeerGone(const AdhocSocket *sock) { + u64 leftAt = peerLeftAt(&sock->data.ptp.paddr); + if (leftAt == 0) { + return false; + } + u64 giveUpUs = (u64)std::max(0, sock->retry_interval) * (u64)std::max(1, sock->retry_count); + return CoreTiming::GetGlobalTimeUsScaled() - leftAt >= giveUpUs; +} + static int pdp_recv_postoffice(int idx, SceNetEtherAddr *saddr, uint16_t *sport, void *data, int *len) { AdhocSocket *internal = adhocSockets[idx]; void *pdp_sock = pdp_postoffice_recover(idx); @@ -861,6 +884,10 @@ static int pdp_recv_postoffice(int idx, SceNetEtherAddr *saddr, uint16_t *sport, if (pdp_recv_status == AEMU_POSTOFFICE_CLIENT_SESSION_WOULD_BLOCK) { return SOCKET_ERROR; } + // Out of the group, nothing from anyone else reaches us (pspautotests net/adhoc/pair/vanish). + if (offAdhocNetwork() && !isLocalMac(&saddr_copy)) { + return SOCKET_ERROR; + } *len = len_copy; if (saddr != NULL) { @@ -1030,6 +1057,11 @@ static int pdp_send_postoffice(int idx, const SceNetEtherAddr *daddr, uint16_t d return SOCKET_ERROR; } + // Out of the group, it goes into the air and reaches nobody (pspautotests net/adhoc/pair/vanish). + if (offAdhocNetwork() && !isLocalMac(daddr)) { + return 0; + } + SceNetEtherAddr fixed_daddr = *daddr; fixGameMac(&fixed_daddr); @@ -1126,6 +1158,14 @@ static int ptp_send_postoffice(int idx, const void *data, int *len) { return SCE_NET_ADHOC_ERROR_WOULD_BLOCK; } + // Out of the group, the send succeeds and the data goes nowhere (pspautotests net/adhoc/pair/vanish). + if (offAdhocNetwork()) { + return 0; + } + if (ptpPeerGone(internal)) { + return SOCKET_ERROR; + } + if (*len > AEMU_POSTOFFICE_PTP_BLOCK_MAX) { // force fragmentation for giant sends *len = AEMU_POSTOFFICE_PTP_BLOCK_MAX; @@ -1242,6 +1282,15 @@ static int ptp_recv_postoffice(int idx, void *data, int *len) { return SCE_NET_ADHOC_ERROR_WOULD_BLOCK; } + // Out of the group, nothing arrives, and the connection doesn't fail either (pspautotests + // net/adhoc/pair/vanish). + if (offAdhocNetwork()) { + return SCE_NET_ADHOC_ERROR_WOULD_BLOCK; + } + if (ptpPeerGone(internal)) { + return SOCKET_ERROR; + } + int len_copy = *len; int ptp_recv_status = ptp_recv(internal->postofficeHandle, (char *)data, &len_copy, true); @@ -1403,6 +1452,10 @@ static int ptp_accept_postoffice(int idx, SceNetEtherAddr *saddr, uint16_t *spor ptp_close(new_ptp_socket); return SCE_NET_ADHOC_ERROR_WOULD_BLOCK; } + memset(internal, 0, sizeof(AdhocSocket)); + internal->retry_interval = adhocSockets[idx]->retry_interval; + internal->retry_count = adhocSockets[idx]->retry_count; + internal->attemptCount = 1; // Connected, so a closed socket reports DISCONNECTED (as the native accept does) internal->type = SOCK_PTP; internal->postofficeHandle = new_ptp_socket; @@ -2346,6 +2399,7 @@ static int pdp_create_postoffice(const SceNetEtherAddr *saddr, int sport, int bu if (internal == NULL) { return hleLogDebug(Log::sceNet, ERROR_NET_NO_SPACE, "net no space"); } + memset(internal, 0, sizeof(AdhocSocket)); internal->type = SOCK_PDP; internal->postofficeHandle = NULL; @@ -4764,12 +4818,15 @@ static uint16_t get_random_unused_ptp_port() { return test_port; } -static int ptp_open_postoffice(const SceNetEtherAddr *saddr, uint16_t sport, const SceNetEtherAddr *daddr, uint16_t dport, uint32_t bufsize) { +static int ptp_open_postoffice(const SceNetEtherAddr *saddr, uint16_t sport, const SceNetEtherAddr *daddr, uint16_t dport, uint32_t bufsize, uint32_t rexmt_int, int rexmt_cnt) { AdhocSocket *internal = (AdhocSocket *)malloc(sizeof(AdhocSocket)); if (internal == NULL) { ERROR_LOG(Log::sceNet, "%s: ran out of heap memory trying to open ptp socket", __func__); return ERROR_NET_NO_SPACE; } + memset(internal, 0, sizeof(AdhocSocket)); + internal->retry_interval = rexmt_int; + internal->retry_count = rexmt_cnt; internal->type = SOCK_PTP; internal->postofficeHandle = NULL; @@ -4877,7 +4934,7 @@ static int sceNetAdhocPtpOpen(MIPSState *mips, const char *srcmac, int sport, co // Valid Arguments if (bufsize > 0 && rexmt_int >= 0 && rexmt_cnt >= 0) { if (serverHasRelay) - return ptp_open_postoffice(saddr, sport, daddr, dport, bufsize); + return ptp_open_postoffice(saddr, sport, daddr, dport, bufsize, rexmt_int, rexmt_cnt); // Create Infrastructure Socket (?) // Socket is remapped through adhocSockets @@ -5492,7 +5549,7 @@ static int sceNetAdhocPtpClose(MIPSState *mips, int id, int unknown) { return NetAdhocPtp_Close(id, unknown); } -static int ptp_listen_postoffice(const SceNetEtherAddr *saddr, uint16_t sport, uint32_t bufsize) { +static int ptp_listen_postoffice(const SceNetEtherAddr *saddr, uint16_t sport, uint32_t bufsize, uint32_t rexmt_int, int rexmt_cnt) { if (relayDisabled) { return ERROR_NET_NO_SPACE; } @@ -5503,6 +5560,9 @@ static int ptp_listen_postoffice(const SceNetEtherAddr *saddr, uint16_t sport, u ERROR_LOG(Log::sceNet, "%s: out of heap memory when creating ptp listen socket", __func__); return ERROR_NET_NO_SPACE; } + memset(internal, 0, sizeof(AdhocSocket)); + internal->retry_interval = rexmt_int; + internal->retry_count = rexmt_cnt; internal->postofficeHandle = NULL; internal->type = SOCK_PTP; @@ -5596,7 +5656,7 @@ static int sceNetAdhocPtpListen(MIPSState *mips, const char *srcmac, int sport, if (bufsize > 0 && rexmt_int >= 0 && rexmt_cnt >= 0 && backlog >= 0) { if (serverHasRelay) - return ptp_listen_postoffice(saddr, sport, bufsize); + return ptp_listen_postoffice(saddr, sport, bufsize, rexmt_int, rexmt_cnt); // Create Infrastructure Socket (?) // Socket is remapped through adhocSockets diff --git a/Core/HLE/sceNetAdhocMatching.cpp b/Core/HLE/sceNetAdhocMatching.cpp index 248bde5c6c..9af8deccff 100644 --- a/Core/HLE/sceNetAdhocMatching.cpp +++ b/Core/HLE/sceNetAdhocMatching.cpp @@ -63,6 +63,10 @@ void ZeroNetAdhocMatchingThreads() { int netAdhocMatchingStarted = 0; +// Added to the PSP's timeout for a silent peer, for latency over the internet. 0 keeps the hardware's +// timeout: it already allows keepalive_int * init_count of silence. +static constexpr u64 MATCHING_TIMEOUT_EXTRA_US = 0; + void __UpdateMatchingHandler(const MatchingArgs &ArgsPtr) { std::lock_guard adhocGuard(adhocEvtMtx); @@ -439,9 +443,25 @@ void sendBulkDataPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * // The aemu protocol has no acknowledgement, so DATA_ACK is made here. The peer stays busy until // it's delivered: a SendData before that gets DATA_BUSY, as on a PSP (pspautotests // net/adhoc/pair/matching). + // A peer that has left the group can't acknowledge, so DATA_TIMEOUT comes once the retransmissions + // have run out (pspautotests net/adhoc/pair/vanish). + if (peerLeftAt(mac) != 0) { + peer->dataTimeoutAt = CoreTiming::GetGlobalTimeUsScaled() + (u64)context->resend_int * (u64)std::max(1, context->resendcounter); + return; + } spawnLocalEvent(context, PSP_ADHOC_MATCHING_EVENT_DATA_ACK, mac, 0, NULL); } +static void handleDataTimeouts(SceNetAdhocMatchingContext *context, u64 now) { + std::lock_guard peer_guard(peerlock); + for (SceNetAdhocMatchingMemberInternal *peer = context->peerlist; peer != NULL; peer = peer->next) { + if (peer->dataTimeoutAt != 0 && now >= peer->dataTimeoutAt) { + peer->dataTimeoutAt = 0; + spawnLocalEvent(context, PSP_ADHOC_MATCHING_EVENT_DATA_TIMEOUT, &peer->mac, 0, NULL); + } + } +} + /** * Tell Established Peers of new Child * @param context Matching Context Pointer @@ -1328,6 +1348,8 @@ int matchingInputThread(int matchingId) { // TODO: The MatchingInput thread is u } } + handleDataTimeouts(context, now); + // Ping Required if (context->keepalive_int > 0) { if (static_cast(now - lastping) >= static_cast(context->keepalive_int)) { @@ -1838,8 +1860,9 @@ static int sceNetAdhocMatchingCreate(MIPSState *mips, int mode, int maxnum, int context->hello_int = hello_int; // client might set this to 0 if (keepalive_int < 1) context->keepalive_int = PSP_ADHOCCTL_PING_TIMEOUT; else context->keepalive_int = keepalive_int; // client might set this to 0 context->keepalivecounter = init_count; // used to multiply keepalive_int as timeout - context->timeout = (((u64)(keepalive_int)+(u64)rexmt_int) * (u64)init_count); - context->timeout += 500000; // For internet play we need higher timeout than what the game wanted + // With keepalive 500ms and 5 tries, a PSP times out a silent peer within 3 seconds + // (pspautotests net/adhoc/pair/vanish). + context->timeout = (u64)context->keepalive_int * (u64)std::max(1, init_count) + MATCHING_TIMEOUT_EXTRA_US; context->handler = handler; context->peerPort = new std::map(); @@ -2697,7 +2720,7 @@ void __NetMatchingCallbacks() { //(int matchingId) } else if (peer == NULL || (args[1] != PSP_ADHOC_MATCHING_EVENT_HELLO || (peer->state != PSP_ADHOC_MATCHING_PEER_OUTGOING_REQUEST && peer->state != PSP_ADHOC_MATCHING_PEER_INCOMING_REQUEST && peer->state != PSP_ADHOC_MATCHING_PEER_CANCEL_IN_PROGRESS))) { DEBUG_LOG(Log::sceNet, "AdhocMatchingCallback: [ID=%i][EVENT=%i][%s]", args[0], args[1], mac2str((SceNetEtherAddr *)Memory::GetPointerOrException(args[2])).c_str()); - if (args[1] == PSP_ADHOC_MATCHING_EVENT_DATA_ACK && peer != NULL) { + if ((args[1] == PSP_ADHOC_MATCHING_EVENT_DATA_ACK || args[1] == PSP_ADHOC_MATCHING_EVENT_DATA_TIMEOUT) && peer != NULL) { peer->sending = 0; } if (args[1] == PSP_ADHOC_MATCHING_EVENT_ESTABLISHED && peer != NULL) { From ee86058ec7b02c0c37a2ba1ddcf3f8e067f2c8eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 28 Oct 2026 19:04:59 +0100 Subject: [PATCH 2/3] sceNetAdhocctl: Leave the console out of its own peer lookups; give groups their own BSSID From the basic and lifecycle pair tests, recorded on two PSPs: - GetNameByAddr for the console's own MAC gives NO_ENTRY, and GetAddrByName doesn't find the console itself. - GetPeerInfo with a size too small for the entry gives INVALID_BUFLEN. - A PTP connection accepted over the relay reports the peer's port as the game sees it, not the relay's offset port. - The built-in server makes up a locally administered BSSID for each group, which stays when the creator leaves. It was the MAC of the oldest member. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/proAdhoc.cpp | 3 +-- Core/HLE/proAdhocServer.cpp | 27 +++++++++++++-------------- Core/HLE/proAdhocServer.h | 3 +++ Core/HLE/sceNetAdhoc.cpp | 37 +++++++++---------------------------- 4 files changed, 26 insertions(+), 44 deletions(-) diff --git a/Core/HLE/proAdhoc.cpp b/Core/HLE/proAdhoc.cpp index b249b35d69..f94f599465 100644 --- a/Core/HLE/proAdhoc.cpp +++ b/Core/HLE/proAdhoc.cpp @@ -2258,8 +2258,7 @@ int getNicknameCount(const char * nickname) // Counter int count = 0; - // Local Nickname Matches - if (strncmp((char *)¶meter.nickname.data, nickname, ADHOCCTL_NICKNAME_LEN) == 0) count++; + // The console itself isn't in the peer list (pspautotests net/adhoc/pair/basic). // Peer Reference SceNetAdhocctlPeerInfo * peer = friends; diff --git a/Core/HLE/proAdhocServer.cpp b/Core/HLE/proAdhocServer.cpp index 2921dc793f..012bb2c0cc 100644 --- a/Core/HLE/proAdhocServer.cpp +++ b/Core/HLE/proAdhocServer.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -813,6 +814,15 @@ void connect_user(SceNetAdhocctlUserNode * user, SceNetAdhocctlGroupName * group // Copy Group Name g->group = *group; + // A PSP group's BSSID is a locally administered address of its own, not a member's MAC. It + // stays when the creator leaves, and a group created again gets a new one (pspautotests + // net/adhoc/pair/lifecycle). + static std::mt19937 bssidRng(std::random_device{}()); + for (int i = 0; i < ETHER_ADDR_LEN; i++) { + g->bssid.data[i] = (uint8_t)bssidRng(); + } + g->bssid.data[0] = (g->bssid.data[0] & 0xFC) | 0x02; + // Increase Group Counter for Game g->game->groupcount++; } @@ -860,13 +870,12 @@ void connect_user(SceNetAdhocctlUserNode * user, SceNetAdhocctlGroupName * group iResult = (int)send(user->stream, (const char*)&packet, sizeof(packet), MSG_NOSIGNAL); if (iResult < 0) ERROR_LOG(Log::sceNet, "AdhocServer: connect_user[send user] (Socket error %d)", socket_errno); - // Set BSSID - if(peer->group_next == NULL) bssid.mac = peer->resolver.mac; - // Move Pointer peer = peer->group_next; } + bssid.mac = g->bssid; + // Link User to Group user->group_next = g->player; if(g->player != NULL) g->player->group_prev = user; @@ -1057,17 +1066,7 @@ void send_scan_results(SceNetAdhocctlUserNode * user) // Set Group Name packet.group = group->group; - // Iterate Players in Network Group - SceNetAdhocctlUserNode * peer = group->player; - for(; peer != NULL; peer = peer->group_next) - { - // Found Network Founder - if(peer->group_next == NULL) - { - // Set Group Host MAC - packet.mac = peer->resolver.mac; - } - } + packet.mac = group->bssid; // Send Group Packet int iResult = (int)send(user->stream, (const char*)&packet, sizeof(packet), MSG_NOSIGNAL); diff --git a/Core/HLE/proAdhocServer.h b/Core/HLE/proAdhocServer.h index 63a2897652..df2ff77b0e 100644 --- a/Core/HLE/proAdhocServer.h +++ b/Core/HLE/proAdhocServer.h @@ -262,6 +262,9 @@ struct SceNetAdhocctlGroupNode { // Double-Linked Player List SceNetAdhocctlUserNode * player; + + // Made up when the group is created, and kept while it lasts + SceNetEtherAddr bssid; }; // User Count diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 77abd85e77..d766b00d8d 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -1462,7 +1462,7 @@ static int ptp_accept_postoffice(int idx, SceNetEtherAddr *saddr, uint16_t *spor internal->data.ptp.laddr = adhocSockets[idx]->data.ptp.laddr; internal->data.ptp.lport = adhocSockets[idx]->data.ptp.lport; internal->data.ptp.paddr = mac_cpy; - internal->data.ptp.pport = port_cpy; + internal->data.ptp.pport = reverse_port_simple(port_cpy); internal->data.ptp.state = ADHOC_PTP_STATE_ESTABLISHED; internal->data.ptp.rcv_sb_cc = adhocSockets[idx]->data.ptp.rcv_sb_cc; internal->data.ptp.snd_sb_cc = adhocSockets[idx]->data.ptp.snd_sb_cc; @@ -4055,16 +4055,11 @@ static int sceNetAdhocctlGetNameByAddr(MIPSState *mips, const char *mac, u32 nam SceNetEtherAddr localmac; getLocalMac(&localmac); - // Local MAC Matches + // The console itself isn't in the peer list: NO_ENTRY, and the name isn't written (pspautotests + // net/adhoc/pair/basic). if (isMacMatch(&localmac, (const SceNetEtherAddr*)mac)) { - // Write Data - *nickname = parameter.nickname; - - DEBUG_LOG(Log::sceNet, "sceNetAdhocctlGetNameByAddr - [PlayerName:%s]", (char*)nickname); - - // Return Success - return 0; + return hleLogDebug(Log::sceNet, SCE_NET_ADHOC_ERROR_NO_ENTRY, "local MAC"); } // Multithreading Lock @@ -4120,7 +4115,9 @@ int sceNetAdhocctlGetPeerInfo(MIPSState *mips, const char *mac, int size, u32 pe } // Library initialized if (netAdhocctlInited) { - if ((size < (int)sizeof(SceNetAdhocctlPeerInfoEmu)) || (buf == NULL)) return SCE_NET_ADHOCCTL_ERROR_INVALID_ARG; + if (buf == NULL) return SCE_NET_ADHOCCTL_ERROR_INVALID_ARG; + // A size too small for the entry gives the ad hoc library's INVALID_BUFLEN (pspautotests net/adhoc/pair/basic). + if (size < (int)sizeof(SceNetAdhocctlPeerInfoEmu)) return hleLogError(Log::sceNet, SCE_NET_ADHOC_ERROR_INVALID_BUFLEN, "buffer too small"); int retval = SCE_NET_ADHOC_ERROR_NO_ENTRY; // -1; @@ -6735,24 +6732,8 @@ static int sceNetAdhocctlGetAddrByName(MIPSState *mips, const char *nickName, u3 // Minimum Space available if (requestcount > 0) { - // Local Nickname Matches - if (strncmp((char *)¶meter.nickname.data, nickName, ADHOCCTL_NICKNAME_LEN) == 0) - { - // Get Local IP Address - sockaddr_in addr; - SceNetEtherAddr mac; - - getLocalIp(&addr); - buf[discovered].nickname = parameter.nickname; - buf[discovered].nickname.data[ADHOCCTL_NICKNAME_LEN - 1] = 0; // last char need to be null-terminated char - getLocalMac(&mac); - buf[discovered].mac_addr = mac; - buf[discovered].flags = 0x0400; - u64 lastrecv = std::max(0LL, static_cast(CoreTiming::GetGlobalTimeUsScaled() - defaultLastRecvDelta)); - buf[discovered++].last_recv = lastrecv; - - DEBUG_LOG(Log::sceNet, "Peer [%s][%s][%s][%llu]", mac2str(&mac).c_str(), ip2str(addr.sin_addr).c_str(), nickName, lastrecv); - } + // The console itself isn't in the peer list, so it's never found here (pspautotests + // net/adhoc/pair/basic). // Peer Reference SceNetAdhocctlPeerInfo * peer = friends; From f449129dec6bb7f12bf2844822438e7de1976c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 29 Oct 2026 07:57:16 +0100 Subject: [PATCH 3/3] sceNetAdhoc: Time a vanished peer's timeouts as a PSP does A PSP checks a PTP 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. A matching peer times out keepalive_int * init_count after its last keepalive, checked all the time instead of on the console's own keepalive tick, which could add up to an interval. Both measured with several settings in pspautotests net/adhoc/pair/vanishtiming; DATA_TIMEOUT already matched. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/proAdhoc.h | 1 + Core/HLE/sceNetAdhoc.cpp | 43 +++++++++++++++++++++----------- Core/HLE/sceNetAdhocMatching.cpp | 14 ++++------- pspautotests | 2 +- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/Core/HLE/proAdhoc.h b/Core/HLE/proAdhoc.h index 09547f2ba0..fb62ee93ab 100644 --- a/Core/HLE/proAdhoc.h +++ b/Core/HLE/proAdhoc.h @@ -325,6 +325,7 @@ typedef struct AdhocSocket { s32 attemptCount; // connect/accept attempts 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 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 d766b00d8d..83e5396c5d 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -852,15 +852,30 @@ static bool isLocalMac(const SceNetEtherAddr *mac) { return isMacMatch(&localMac, mac); } -// A PTP connection to a peer that has left the group fails once the retransmissions would have run out: -// rexmt_int * rexmt_cnt after it left (pspautotests net/adhoc/pair/vanish). +static void ptpSetEstablished(AdhocSocket *sock) { + sock->data.ptp.state = ADHOC_PTP_STATE_ESTABLISHED; + sock->ptpEstablishedAt = CoreTiming::GetGlobalTimeUsScaled(); +} + +// 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. static bool ptpPeerGone(const AdhocSocket *sock) { u64 leftAt = peerLeftAt(&sock->data.ptp.paddr); if (leftAt == 0) { return false; } - u64 giveUpUs = (u64)std::max(0, sock->retry_interval) * (u64)std::max(1, sock->retry_count); - return CoreTiming::GetGlobalTimeUsScaled() - leftAt >= giveUpUs; + 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) { + return false; + } + // The last check, at or before now, has to be past the limit. + u64 base = std::min(sock->ptpEstablishedAt, leftAt); + u64 lastCheck = base + (now - base) / interval * interval; + return lastCheck > leftAt + limit; } static int pdp_recv_postoffice(int idx, SceNetEtherAddr *saddr, uint16_t *sport, void *data, int *len) { @@ -1211,7 +1226,7 @@ int DoBlockingPtpSend(AdhocSocketRequest& req, s64& result) { // sent // Set to Established on successful Send when an attempt to Connect was initiated if (ptpsocket.state == ADHOC_PTP_STATE_SYN_SENT) - ptpsocket.state = ADHOC_PTP_STATE_ESTABLISHED; + ptpSetEstablished(sock); DEBUG_LOG(Log::sceNet, "sceNetAdhocPtpSend[%i:%u]: Sent %u bytes to %s:%u\n", req.id, ptpsocket.lport, ret, mac2str(&ptpsocket.paddr).c_str(), ptpsocket.pport); @@ -1243,7 +1258,7 @@ int DoBlockingPtpSend(AdhocSocketRequest& req, s64& result) { // Set to Established on successful Send when an attempt to Connect was initiated if (ptpsocket.state == ADHOC_PTP_STATE_SYN_SENT) - ptpsocket.state = ADHOC_PTP_STATE_ESTABLISHED; + ptpSetEstablished(sock); // Return Success result = 0; @@ -1360,7 +1375,7 @@ int DoBlockingPtpRecv(AdhocSocketRequest& req, s64& result) { // Set to Established on successful Recv when an attempt to Connect was initiated if (ptpsocket.state == ADHOC_PTP_STATE_SYN_SENT) - ptpsocket.state = ADHOC_PTP_STATE_ESTABLISHED; + ptpSetEstablished(sock); result = 0; } @@ -1463,7 +1478,7 @@ static int ptp_accept_postoffice(int idx, SceNetEtherAddr *saddr, uint16_t *spor internal->data.ptp.lport = adhocSockets[idx]->data.ptp.lport; internal->data.ptp.paddr = mac_cpy; internal->data.ptp.pport = reverse_port_simple(port_cpy); - internal->data.ptp.state = ADHOC_PTP_STATE_ESTABLISHED; + ptpSetEstablished(internal); internal->data.ptp.rcv_sb_cc = adhocSockets[idx]->data.ptp.rcv_sb_cc; internal->data.ptp.snd_sb_cc = adhocSockets[idx]->data.ptp.snd_sb_cc; internal->data.ptp.id = AEMU_POSTOFFICE_ID_BASE + idx; @@ -1696,7 +1711,7 @@ int DoBlockingPtpConnect(AdhocSocketRequest& req, s64& result, AdhocSendTargets& // Update Adhoc Socket state if (ret != SOCKET_ERROR || sockerr == EISCONN) { - ptpsocket.state = ADHOC_PTP_STATE_ESTABLISHED; + ptpSetEstablished(sock); INFO_LOG(Log::sceNet, "sceNetAdhocPtpConnect[%i:%u]: Established (%s:%u)", req.id, ptpsocket.lport, ip2str(sin.sin_addr).c_str(), ptpsocket.pport); // Done @@ -3447,7 +3462,7 @@ int PollAdhocSocket(SceNetAdhocPollSd* sds, int count, int timeout, int nonblock struct sockaddr_in peer; socklen_t peerLen = sizeof(peer); if (getpeername(fd, (struct sockaddr *)&peer, &peerLen) == 0) { - sock->data.ptp.state = ADHOC_PTP_STATE_ESTABLISHED; + ptpSetEstablished(sock); } } if (sock->type == SOCK_PTP && sock->data.ptp.state == ADHOC_PTP_STATE_LISTEN) { @@ -4632,7 +4647,7 @@ static int sceNetAdhocGetPtpStat(MIPSState *mips, u32 structSize, u32 structAddr if (serverHasRelay) { if (sock->postofficeHandle != NULL) { // good to go - sock->data.ptp.state = ADHOC_PTP_STATE_ESTABLISHED; + ptpSetEstablished(sock); } } else { if (IsSocketReady(sock->data.ptp.id, true, true) > 0) { @@ -4641,7 +4656,7 @@ static int sceNetAdhocGetPtpStat(MIPSState *mips, u32 structSize, u32 structAddr 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) { - sock->data.ptp.state = ADHOC_PTP_STATE_ESTABLISHED; + ptpSetEstablished(sock); } } } @@ -5153,7 +5168,7 @@ int AcceptPtpSocket(int ptpId, int newsocket, sockaddr_in& peeraddr, SceNetEther internal->data.ptp.pport = ntohs(peeraddr.sin_port) - portOffset; // Set Connection State - internal->data.ptp.state = ADHOC_PTP_STATE_ESTABLISHED; + ptpSetEstablished(internal); // Return Peer Address & Port Information if (addr != NULL) @@ -5321,7 +5336,7 @@ int NetAdhocPtp_Connect(int id, int timeout, int flag, bool allowForcedConnect) struct sockaddr_in peer; socklen_t peerLen = sizeof(peer); if (getpeername(ptpsocket.id, (struct sockaddr *)&peer, &peerLen) == 0) { - ptpsocket.state = ADHOC_PTP_STATE_ESTABLISHED; + ptpSetEstablished(socket); } } diff --git a/Core/HLE/sceNetAdhocMatching.cpp b/Core/HLE/sceNetAdhocMatching.cpp index 9af8deccff..c906061129 100644 --- a/Core/HLE/sceNetAdhocMatching.cpp +++ b/Core/HLE/sceNetAdhocMatching.cpp @@ -1350,12 +1350,13 @@ int matchingInputThread(int matchingId) { // TODO: The MatchingInput thread is u handleDataTimeouts(context, now); + // A PSP times out a peer exactly keepalive_int * init_count after its last keepalive, not on + // its own keepalive tick (pspautotests net/adhoc/pair/vanishtiming). + handleTimeout(context); + // Ping Required if (context->keepalive_int > 0) { if (static_cast(now - lastping) >= static_cast(context->keepalive_int)) { - // Handle Peer Timeouts - handleTimeout(context); - // Broadcast Ping Message broadcastPingMessage(context); @@ -1363,10 +1364,6 @@ int matchingInputThread(int matchingId) { // TODO: The MatchingInput thread is u lastping = now; } } - else { - // FIXME: Should we checks for Timeout too when the game doesn't set the keep alive interval? - handleTimeout(context); - } // Messages on Stack ready for processing if (context->input_stack != NULL) { @@ -1860,8 +1857,7 @@ static int sceNetAdhocMatchingCreate(MIPSState *mips, int mode, int maxnum, int context->hello_int = hello_int; // client might set this to 0 if (keepalive_int < 1) context->keepalive_int = PSP_ADHOCCTL_PING_TIMEOUT; else context->keepalive_int = keepalive_int; // client might set this to 0 context->keepalivecounter = init_count; // used to multiply keepalive_int as timeout - // With keepalive 500ms and 5 tries, a PSP times out a silent peer within 3 seconds - // (pspautotests net/adhoc/pair/vanish). + // Timed from the peer's last keepalive (pspautotests net/adhoc/pair/vanishtiming). context->timeout = (u64)context->keepalive_int * (u64)std::max(1, init_count) + MATCHING_TIMEOUT_EXTRA_US; context->handler = handler; context->peerPort = new std::map(); diff --git a/pspautotests b/pspautotests index 8ba5805298..a340d72b02 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit 8ba58052984825365a94ec42fb2e0e42bb661231 +Subproject commit a340d72b0243a5bd417d6bfe78988155bbe1ad11