From c1c815da80d6a0a749199ad79c4ee9b1a16271cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 3 Nov 2026 15:05:55 +0100 Subject: [PATCH 1/2] Ad hoc relay: Deliver back-to-back PTP sends about 1 ms apart On a PSP, two PTP sends made back to back arrive about 1 ms apart (0.95 ms plus 0.65 us a byte), so a receiver polling fast reads them one at a time; once both have arrived they come back as one read (pspautotests net/adhoc/pair/ptpsegments, ptpmsg). Through the relay both arrived together, and Capcom Classics Collection Reloaded's Street Fighter II, which expects them one at a time, dropped the connection as the match started. Relay PTP sessions now run in aemu_postoffice's block boundary mode, which hands over one send at a time. Each block goes into a queue on the socket, up to its buffer size, and becomes readable no earlier than the previous one plus the PSP's gap, counted from when it arrived; the relay's 1 ms poll takes blocks as they come, so a receiver that looks late still gets them all at once. P2P mode can't do this: its TCP stream has no send boundaries. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/AdhocTransportRelay.cpp | 126 ++++++++++++++++++++++++++----- ext/aemu_postoffice | 2 +- pspautotests | 2 +- 3 files changed, 109 insertions(+), 21 deletions(-) diff --git a/Core/HLE/AdhocTransportRelay.cpp b/Core/HLE/AdhocTransportRelay.cpp index fbf49569bd..15b039a0d0 100644 --- a/Core/HLE/AdhocTransportRelay.cpp +++ b/Core/HLE/AdhocTransportRelay.cpp @@ -50,6 +50,7 @@ #endif static void closeRelayBacklog(const AdhocSocket *listener); +static void closeRelayPtpQueue(const AdhocSocket *sock); void RelayTransport::CloseHostSocket(AdhocSocket *sock) { if (sock->type == SOCK_PTP) { @@ -67,6 +68,7 @@ void RelayTransport::CloseHostSocket(AdhocSocket *sock) { } } closeRelayBacklog(sock); + closeRelayPtpQueue(sock); } else { if (sock->postofficeHandle != NULL) { pdp_delete(sock->postofficeHandle); @@ -311,6 +313,75 @@ int RelayTransport::DoBlockingPtpSend(AdhocSocketRequest &req, s64 &result) { return 0; } +// A PSP's back-to-back PTP sends arrive about a millisecond apart, 0.95 ms plus 0.65 us a byte, so a receiver +// polling fast reads them one at a time; once they have all arrived they come back together (pspautotests +// net/adhoc/pair/ptpsegments, ptpmsg). Capcom Classics Collection Reloaded's Street Fighter II needs the +// split. So the relay session runs in block boundary mode, each block it hands over is queued here, and a +// block becomes readable no earlier than the previous one plus that gap, counted from when it arrived (the +// 1 ms poll takes blocks as they come). The queue holds up to the socket's buffer size, so a receiver that +// stops reading still holds the sender back. +struct RelayPtpBlock { + std::vector data; + u64 readableAt; +}; + +struct RelayPtpQueue { + std::deque blocks; + size_t offset = 0; // already read from the first block + size_t bytes = 0; // left in all of them + u64 lastReadableAt = 0; +}; + +static std::map relayPtpQueues; + +static void closeRelayPtpQueue(const AdhocSocket *sock) { + relayPtpQueues.erase(sock); +} + +// Takes the blocks that have arrived. Returns AEMU_POSTOFFICE_CLIENT_SESSION_DEAD once the session has ended. +static int fillRelayPtpQueue(AdhocSocket *sock) { + RelayPtpQueue &queue = relayPtpQueues[sock]; + size_t limit = std::max(sock->buffer_size, (u32)1); + while (queue.bytes < limit) { + int next = ptp_peek_next_size(sock->postofficeHandle); + if (next == AEMU_POSTOFFICE_CLIENT_SESSION_DEAD) { + return next; + } + if (next <= 0) { + break; + } + RelayPtpBlock block; + block.data.resize(next); + int len = next; + int result = ptp_recv(sock->postofficeHandle, (char *)block.data.data(), &len, true); + if (result == AEMU_POSTOFFICE_CLIENT_SESSION_DEAD) { + return result; + } + if (result != AEMU_POSTOFFICE_CLIENT_OK) { + break; + } + block.data.resize(len); + u64 now = CoreTiming::GetGlobalTimeUsScaled(); + block.readableAt = now; + if (queue.lastReadableAt != 0) { + block.readableAt = std::max(now, queue.lastReadableAt + 950 + (u64)len * 13 / 20); + } + queue.lastReadableAt = block.readableAt; + queue.bytes += len; + queue.blocks.push_back(std::move(block)); + } + return 0; +} + +static size_t relayPtpReadable(const RelayPtpQueue &queue) { + u64 now = CoreTiming::GetGlobalTimeUsScaled(); + size_t bytes = 0; + for (size_t i = 0; i < queue.blocks.size() && queue.blocks[i].readableAt <= now; ++i) { + bytes += queue.blocks[i].data.size() - (i == 0 ? queue.offset : 0); + } + return bytes; +} + static int ptp_recv_postoffice(int idx, void *data, int *len) { // Out of the group, nothing arrives, and the connection doesn't fail either (pspautotests // net/adhoc/pair/vanish), whatever became of the relay meanwhile. @@ -333,22 +404,32 @@ static int ptp_recv_postoffice(int idx, void *data, int *len) { return SOCKET_ERROR; } - int len_copy = *len; - - int ptp_recv_status = ptp_recv(internal->postofficeHandle, (char *)data, &len_copy, true); - if (ptp_recv_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 - //handle_relay_connect_failure(); - return SOCKET_ERROR; - } - if (ptp_recv_status == AEMU_POSTOFFICE_CLIENT_SESSION_WOULD_BLOCK) { + int fill = fillRelayPtpQueue(internal); + RelayPtpQueue &queue = relayPtpQueues[internal]; + size_t readable = relayPtpReadable(queue); + if (readable == 0) { + // What arrived before the other side closed is still delivered (pspautotests net/adhoc/pair/ptpclose). + if (fill == AEMU_POSTOFFICE_CLIENT_SESSION_DEAD && queue.bytes == 0) { + return SOCKET_ERROR; + } return SCE_NET_ADHOC_ERROR_WOULD_BLOCK; } - // AEMU_POSTOFFICE_CLIENT_SESSION_DATA_TRUNC is okay, it just means it has data in it's user space buffer - - *len = len_copy; + size_t want = std::min((size_t)std::max(*len, 0), readable); + size_t copied = 0; + while (copied < want) { + RelayPtpBlock &block = queue.blocks.front(); + size_t n = std::min(want - copied, block.data.size() - queue.offset); + memcpy((u8 *)data + copied, block.data.data() + queue.offset, n); + copied += n; + queue.offset += n; + queue.bytes -= n; + if (queue.offset == block.data.size()) { + queue.blocks.pop_front(); + queue.offset = 0; + } + } + *len = (int)copied; return 0; } @@ -437,7 +518,7 @@ static void fillRelayBacklog(int idx) { } int state = 0; RelayPendingConnection connection = {}; - connection.handle = ptp_accept(ptp_listen_socket, (char *)&connection.mac, &connection.port, true, NULL, NULL, &state); + connection.handle = ptp_accept(ptp_listen_socket, (char *)&connection.mac, &connection.port, true, true, NULL, NULL, &state); if (connection.handle == NULL) { if (state == AEMU_POSTOFFICE_CLIENT_SESSION_DEAD) { handle_relay_connect_failure(); @@ -503,6 +584,12 @@ static const double RELAY_CONNECT_WAIT_SECONDS = 1.0; void RelayTransport::Poll() { fillRelayBacklogs(); + for (int i = 0; i < MAX_SOCKET; i++) { + AdhocSocket *sock = adhocSockets[i]; + if (sock != NULL && sock->type == SOCK_PTP && sock->data.ptp.state == ADHOC_PTP_STATE_ESTABLISHED && sock->postofficeHandle != NULL) { + fillRelayPtpQueue(sock); + } + } // A connect takes a few round trips through the relay, in real time, and the PSP has an opened // connection up a few ms after PtpOpen (pspautotests net/adhoc/pair/ptppoll). So wait for it here, at @@ -638,7 +725,7 @@ static int ptp_connect_postoffice(int idx, const char *caller) { int state; SceNetEtherAddr fixed_daddr = internal->data.ptp.paddr; fixGameMac(&fixed_daddr); - void *ptp_socket = ptp_connect_v4(&addr, (const char *)&internal->data.ptp.laddr, offset_port_simple(internal->data.ptp.lport), (const char *)&fixed_daddr, offset_port_simple(internal->data.ptp.pport), netAdhocUseUdp, NULL, NULL, &state); + void *ptp_socket = ptp_connect_v4(&addr, (const char *)&internal->data.ptp.laddr, offset_port_simple(internal->data.ptp.lport), (const char *)&fixed_daddr, offset_port_simple(internal->data.ptp.pport), netAdhocUseUdp, true, NULL, NULL, &state); if (ptp_socket == NULL) { internal->connectThreadResult = SCE_NET_ADHOC_ERROR_CONNECTION_REFUSED; ERROR_LOG(Log::sceNet, "%s: failed connecting to ptp socket, %d", __func__, state); @@ -753,12 +840,11 @@ static bool postoffice_can_recv_or_listen_has_request(int idx) { if (ptp_handle == NULL) { return false; } - int peek_len = ptp_peek_next_size(ptp_handle); // A closed connection reads as readable, so the poll looks at PtpPeerClosed. - if (peek_len == AEMU_POSTOFFICE_CLIENT_SESSION_DEAD) { + if (fillRelayPtpQueue(internal) == AEMU_POSTOFFICE_CLIENT_SESSION_DEAD) { return true; } - if (peek_len > 0) { + if (relayPtpReadable(relayPtpQueues[internal]) > 0) { return true; } } @@ -842,7 +928,8 @@ void RelayTransport::PtpUpdateRecvStat(AdhocSocket *sock) { // A listener's handle is a listen session, which has no data. int waiting = 0; if (sock->postofficeHandle != NULL && sock->data.ptp.state != ADHOC_PTP_STATE_LISTEN) { - waiting = ptp_peek_next_size(sock->postofficeHandle); + fillRelayPtpQueue(sock); + waiting = (int)relayPtpReadable(relayPtpQueues[sock]); } sock->data.ptp.rcv_sb_cc = std::max(0, waiting); } @@ -937,6 +1024,7 @@ static int ptp_close_postoffice(int idx) { } } closeRelayBacklog(internal); + closeRelayPtpQueue(internal); adhocSockets[idx] = NULL; free(internal); INFO_LOG(Log::sceNet, "%s: closed ptp socket with id %d", __func__, idx + 1); diff --git a/ext/aemu_postoffice b/ext/aemu_postoffice index 0b2d4e14e3..02abf18fd1 160000 --- a/ext/aemu_postoffice +++ b/ext/aemu_postoffice @@ -1 +1 @@ -Subproject commit 0b2d4e14e3d61d14fcd1505b958e9007bab0d9e2 +Subproject commit 02abf18fd161a1639d9f93c48c4c46638c7c6d15 diff --git a/pspautotests b/pspautotests index 8301390a1e..1a9e6c31cf 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit 8301390a1e42755ffa5c6d5e0a4f1f8248ea48c1 +Subproject commit 1a9e6c31cf31411f82f8a2d659855442f6dfda11 From 92fbf32e19bd064c89a044fafeb01cef7eee9e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 3 Nov 2026 22:03:18 +0100 Subject: [PATCH 2/2] Ad hoc relay: Iterate the PTP block queue instead of indexing it Co-Authored-By: Claude Opus 5.5 --- Core/HLE/AdhocTransportRelay.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Core/HLE/AdhocTransportRelay.cpp b/Core/HLE/AdhocTransportRelay.cpp index 15b039a0d0..25cad5756b 100644 --- a/Core/HLE/AdhocTransportRelay.cpp +++ b/Core/HLE/AdhocTransportRelay.cpp @@ -376,10 +376,14 @@ static int fillRelayPtpQueue(AdhocSocket *sock) { static size_t relayPtpReadable(const RelayPtpQueue &queue) { u64 now = CoreTiming::GetGlobalTimeUsScaled(); size_t bytes = 0; - for (size_t i = 0; i < queue.blocks.size() && queue.blocks[i].readableAt <= now; ++i) { - bytes += queue.blocks[i].data.size() - (i == 0 ? queue.offset : 0); + for (const RelayPtpBlock &block : queue.blocks) { + if (block.readableAt > now) { + break; + } + bytes += block.data.size(); } - return bytes; + // Less what's already been read from the first one. + return bytes == 0 ? 0 : bytes - queue.offset; } static int ptp_recv_postoffice(int idx, void *data, int *len) {