From a2897c5b65e5b3429b61403ca201c2a83035e20b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 28 Oct 2026 15:42:07 +0100 Subject: [PATCH 1/3] sceNetAdhocctl: Deliver CONNECT, SCAN and DISCONNECT at the PSP's times A PSP delivers CONNECT 1.1 s after a Create, 0.53 s after a Join and 3.5-4 s after a Connect (which looks for the group first), SCAN 1.0 s after a Scan and DISCONNECT 105 ms after a Disconnect, and the state changes along with the event (pspautotests net/adhoc/timing and net/adhoc/pair/timing). Deliver them then, counting from the request in emulated time, or later if the server's answer takes longer. This replaces the delays tuned for Dissidia 012, GTA: VCS and Tekken 5, and Disconnect no longer blocks for 16.7 ms (Tekken 6): it returns at once, as on a PSP. Since the state now changes later, a Disconnect can come while a Create is still pending. Keep track of whether the server has us in a group, so the Disconnect still reaches it. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceNetAdhoc.cpp | 66 ++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 7078c3f3cf..5c77d9da23 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -96,6 +96,36 @@ std::map adhocctlHandlers; int adhocctlNotifyEvent = -1; int adhocctlStateEvent = -1; 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 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; + +// How long after the request a PSP delivers an event (pspautotests net/adhoc/timing and +// net/adhoc/pair/timing). The state changes along with it. Over the internet the server's answer can +// come later, and then the event waits for it. +static int PspEventDelayUs(u32 flags) { + switch (flags) { + case ADHOCCTL_EVENT_CONNECT: + switch (adhocConnectionType) { + case ADHOC_CREATE: return 1110000; + case ADHOC_JOIN: return 530000; + // Connect looks for the group first: about 3.5 s to join one, 4 s to create it. + default: return getActivePeerCount() > 0 ? 3500000 : 4000000; + } + case ADHOCCTL_EVENT_SCAN: return 1015000; + case ADHOCCTL_EVENT_DISCONNECT: return 105000; + default: return 0; + } +} + +static int RemainingEventDelayUs(u32 flags) { + s64 left = (s64)(adhocctlRequestTimeUs + PspEventDelayUs(flags)) - (s64)CoreTiming::GetGlobalTimeUs(); + return left > 0 ? (int)left : 0; +} + int adhocSocketNotifyEvent = -1; std::map adhocctlRequests; std::map adhocSocketRequests; @@ -571,6 +601,9 @@ static void __AdhocctlNotify(u64 userdata, int cyclesLate) { switch(req.opcode) { case OPCODE_CONNECT:{ call_status = adhocctl_connect(adhocctlHandle, (char *)req.group.data); + if (call_status == ADHOCCTL_CALL_SUCCESS) { + adhocctlInServerGroup = true; + } break; } case OPCODE_SCAN:{ @@ -682,6 +715,7 @@ int WaitBlockingAdhocctlSocket(AdhocctlRequest request, int usec, const char* re u64 param = ((u64)__KernelGetCurThread()) << 32 | uid; adhocctlStartTime = (u64)(time_now_d() * 1000000.0); + adhocctlRequestTimeUs = CoreTiming::GetGlobalTimeUs(); adhocctlRequests[uid] = request; CoreTiming::ScheduleEvent(usToCycles(usec), adhocctlNotifyEvent, param); __KernelWaitCurThread(WAITTYPE_NET, uid, request.opcode, 0, false, reason); @@ -1910,6 +1944,7 @@ static void StartFriendFinder() { static void StopFriendFinder() { friendFinderRunning = false; friendFinderStop(); + adhocctlInServerGroup = false; int32_t peercount = 0; freeFriendsRecursive(friends, &peercount); @@ -1920,7 +1955,7 @@ static void StopFriendFinder() { } void __NetAdhocDoState(PointerWrap &p) { - auto s = p.Section("sceNetAdhoc", 1, 11); + auto s = p.Section("sceNetAdhoc", 1, 12); if (!s) return; @@ -2019,8 +2054,15 @@ void __NetAdhocDoState(PointerWrap &p) { adhocctlPollEvent = -1; } CoreTiming::RestoreRegisterEvent(adhocctlPollEvent, "__AdhocctlPoll", __AdhocctlPoll); + if (s >= 12) { + Do(p, adhocctlRequestTimeUs); + } else { + adhocctlRequestTimeUs = 0; + } if (p.mode == p.MODE_READ) { + // The login after a load starts outside any group. + adhocctlInServerGroup = false; // Discard leftover events { std::lock_guard adhocGuard(adhocEvtMtx); @@ -3762,15 +3804,15 @@ u32 NetAdhocctl_Disconnect() { // Library initialized if (netAdhocctlInited) { int iResult, error; - // We might need to have at least 16ms (1 frame?) delay before the game calls the next Adhocctl syscall for Tekken 6 not to stuck when exiting Lobby - hleEatMicro(16667); - 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). + adhocctlRequestTimeUs = CoreTiming::GetGlobalTimeUs(); // 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) { + if (adhocctlState != ADHOCCTL_STATE_DISCONNECTED || adhocctlInServerGroup) { + adhocctlInServerGroup = false; isAdhocctlBusy = true; // Clear Network Name @@ -3865,7 +3907,7 @@ static u32 sceNetAdhocctlDelHandler(u32 handlerID) { int NetAdhocctl_Term() { if (netAdhocctlInited) { - if (adhocctlState != ADHOCCTL_STATE_DISCONNECTED) { + if (adhocctlState != ADHOCCTL_STATE_DISCONNECTED || adhocctlInServerGroup) { // Note: This might block current thread if the first attempt to send OPCODE_DISCONNECT to AdhocServer failed with EAGAIN error if (netAdhocGameModeEntered) NetAdhocctl_ExitGameMode(); @@ -3876,6 +3918,7 @@ int NetAdhocctl_Term() { // Stop the adhocctl client friendFinderRunning = false; friendFinderStop(); + adhocctlInServerGroup = false; // TODO: May need to block current thread to make sure all Adhocctl callbacks have been fully executed before terminating Adhoc PSPThread (ie. threadAdhocID). @@ -6304,18 +6347,15 @@ void __NetTriggerCallbacks() switch (flags) { case ADHOCCTL_EVENT_CONNECT: newState = ADHOCCTL_STATE_CONNECTED; - if (adhocConnectionType == ADHOC_CREATE) - delayus = adhocEventDelay; // May affects Dissidia 012 and GTA VCS - else if (adhocConnectionType == ADHOC_CONNECT) - delayus = adhocEventDelay / 2; + delayus = RemainingEventDelayUs(flags); break; - case ADHOCCTL_EVENT_SCAN: // notified only when scan completed? + case ADHOCCTL_EVENT_SCAN: newState = ADHOCCTL_STATE_DISCONNECTED; - //delayus = adhocEventDelay / 2; + delayus = RemainingEventDelayUs(flags); break; case ADHOCCTL_EVENT_DISCONNECT: newState = ADHOCCTL_STATE_DISCONNECTED; - delayus = adhocDefaultDelay; // Tekken 5 expects AdhocctlDisconnect to be done within ~17ms (a frame?) + delayus = RemainingEventDelayUs(flags); break; case ADHOCCTL_EVENT_GAME: { From 1e3e9c7251f4bd62f96a7745584e0a228e060910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 28 Oct 2026 15:42:07 +0100 Subject: [PATCH 2/3] sceNetAdhocctl: Send Create, Connect and Scan without the 10 ms wait MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a PSP they return within about 30 µs (pspautotests net/adhoc/timing). They waited 10 ms before trying to send the request; now the first try is right away, and the call returns once it's sent. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceNetAdhoc.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 5c77d9da23..542d5dfb05 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -121,6 +121,10 @@ static int PspEventDelayUs(u32 flags) { } } +// A PSP returns from Create, Connect, Join and Scan within about 30 µs (pspautotests net/adhoc/timing). +// The request goes out to the server then, and the call returns once it's sent. +static const int ADHOCCTL_REQUEST_US = 30; + static int RemainingEventDelayUs(u32 flags) { s64 left = (s64)(adhocctlRequestTimeUs + PspEventDelayUs(flags)) - (s64)CoreTiming::GetGlobalTimeUs(); return left > 0 ? (int)left : 0; @@ -3638,7 +3642,7 @@ int sceNetAdhocctlScan(MIPSState *mips) { if (friendFinderRunning) { AdhocctlRequest req = { OPCODE_SCAN, {0} }; - return hleLogDebugOrError(Log::sceNet, WaitBlockingAdhocctlSocket(req, us, "adhocctl scan")); + return hleLogDebugOrError(Log::sceNet, WaitBlockingAdhocctlSocket(req, ADHOCCTL_REQUEST_US, "adhocctl scan")); } else { adhocctlState = ADHOCCTL_STATE_DISCONNECTED; @@ -4114,10 +4118,9 @@ int NetAdhocctl_Create(const char *groupName) { //setConnectionStatus(1); // Wait for Status to be connected to prevent Ford Street Racing from Failed to create game session - int us = adhocDefaultDelay; if (friendFinderRunning) { AdhocctlRequest req = { OPCODE_CONNECT, parameter.group_name }; - return WaitBlockingAdhocctlSocket(req, us, "adhocctl connect"); + return WaitBlockingAdhocctlSocket(req, ADHOCCTL_REQUEST_US, "adhocctl connect"); } //Faking success, to prevent Full Auto 2 from freezing while Initializing Network else { @@ -4134,7 +4137,7 @@ int NetAdhocctl_Create(const char *groupName) { } } - hleEatMicro(us); + hleEatMicro(adhocDefaultDelay); // Return Success // FIXME: When tested using JPCSP + official prx files it seems sceNetAdhocctlCreate switching to a different thread for at least 100ms after returning success and before executing the next line. return 0; From 84c9f2d38d543b54d6a3d14f8c955ac141547554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 28 Oct 2026 15:45:07 +0100 Subject: [PATCH 3/3] adhoc_multi: Show the output of tests that haven't been recorded yet Co-Authored-By: Claude Opus 5.5 --- Tools/adhoc_multi.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Tools/adhoc_multi.py b/Tools/adhoc_multi.py index 331ef37129..9644757ffc 100644 --- a/Tools/adhoc_multi.py +++ b/Tools/adhoc_multi.py @@ -61,17 +61,21 @@ AdhocServerRelayMode = 1 def run_test(headless, test, extra, relay_ini): roles = roles_of(test) memstick = os.path.join(os.path.dirname(headless), "memstick") + # A test that hasn't been recorded on PSPs yet has no expected files; then just show what each instance prints. + compare = all(os.path.exists(os.path.join(TESTS_DIR, "%s_%s.expected" % (test, role))) for role in roles) procs = [] for i, role in enumerate(roles): - cmd = [headless, "--root", os.path.join(ROOT, "pspautotests") + "/", "--compare", "--timeout-wall=%d" % TIMEOUT, + cmd = [headless, "--root", os.path.join(ROOT, "pspautotests") + "/", "--timeout-wall=%d" % TIMEOUT, "--graphics=software", "--memstick=" + memstick, "--realtime", "--instance=%d" % (i + 1)] + if compare: + cmd.append("--compare") if relay_ini: cmd.append("--appendconfig=" + relay_ini) if i == 0: cmd.append("--adhoc-server") cmd += extra cmd.append(os.path.join(TESTS_DIR, "%s_%s.prx" % (test, role))) - procs.append((role, subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT))) + procs.append((role, subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT if compare else subprocess.DEVNULL))) if i == 0: # Let the host's built-in server come up before the clients log in to it. time.sleep(1.0) @@ -79,6 +83,11 @@ def run_test(headless, test, extra, relay_ini): passed = True for role, proc in procs: out, _ = proc.communicate(timeout=TIMEOUT + 30) + if not compare: + print("OUTPUT %s_%s (no expected file):" % (test, role)) + sys.stdout.write(out.decode("utf-8", "replace")) + passed = False + continue ok = proc.returncode == 0 passed = passed and ok print("%s %s_%s" % ("PASS" if ok else "FAIL", test, role))