From af8088b7908b8b0cacf7fd87a6bf6d675bbb6b15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 29 Sep 2026 10:33:56 -0600 Subject: [PATCH] Threads: A delay can end before it starts waiting A delay's deadline is now + usec, and the clock is read again when the alarm is set. If the deadline has passed by then, the call returns 0 at once without giving up the CPU. On hardware that makes sceKernelDelayThread(0) return at once about 60% of the time. On a thread's first wait after it starts, a delay of 1 does so about two times in three as well (pspautotests threads/scheduling/delayzero). We always waited at least 210us. The choice is pseudo-random off the tick count, not the tick phase, since our cycle counts are regular enough for a polling loop to lock into never yielding. Threads remember whether they've waited since starting (Thread savestate section version 6). Also moves threads/vpl/create into the passing tests: re-recorded on 6.61, it agrees with what we do for partitions 8 and 9. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/HLE/sceKernelThread.cpp | 38 +++++++++++++++++++++++++++++++----- Core/HLE/sceKernelThread.h | 2 ++ pspautotests | 2 +- test.py | 3 ++- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/Core/HLE/sceKernelThread.cpp b/Core/HLE/sceKernelThread.cpp index f140707808..085581b450 100644 --- a/Core/HLE/sceKernelThread.cpp +++ b/Core/HLE/sceKernelThread.cpp @@ -419,7 +419,7 @@ void PSPThread::Cleanup() { } void PSPThread::DoState(PointerWrap &p) { - auto s = p.Section("Thread", 1, 5); + auto s = p.Section("Thread", 1, 6); if (!s) return; @@ -460,6 +460,11 @@ void PSPThread::DoState(PointerWrap &p) { Do(p, waitingThreads); Do(p, pausedWaits); } + if (s >= 6) { + Do(p, hasWaited); + } else { + hasWaited = true; + } } @@ -1443,6 +1448,7 @@ void __KernelWaitCurThread(WaitType type, SceUID waitID, u32 waitValue, u32 time WARN_LOG_REPORT(Log::sceKernel, "Waiting thread for %d that was already waiting for %d", type, thread->nt.waitType); thread->nt.waitID = waitID; thread->nt.waitType = type; + thread->hasWaited = true; __KernelChangeThreadState(thread, ThreadStatus(THREADSTATUS_WAIT | (thread->nt.status & THREADSTATUS_SUSPEND))); thread->nt.numReleases++; thread->waitInfo.waitValue = waitValue; @@ -1884,6 +1890,7 @@ void __KernelResetThread(PSPThread *t, int lowestPriority) { t->nt.exitStatus = SCE_KERNEL_ERROR_NOT_DORMANT; t->isProcessingCallbacks = false; + t->hasWaited = false; t->currentCallbackId = 0; t->currentMipscallId = 0; t->pendingMipsCalls.clear(); @@ -2606,10 +2613,30 @@ static s64 __KernelDelayThreadUs(u64 usec) { return usec + 10; } +// A delay's deadline is now + usec, and the clock is read again when the alarm is set. If the +// deadline has passed by then, the call returns 0 at once without giving up the CPU. The two reads are about 0.6us apart, so a delay of 0 returns at +// once about 60% of the time, and 1 almost never. On a thread's first wait after it starts, when the +// code is presumably out of the cache, they're about 1.65us apart: a delay of 1 returns at once +// about two times in three, and 2 never (pspautotests threads/scheduling/delayzero). +// Our cycle counts are too regular to use the tick phase (a polling loop could lock into never +// yielding), so it's pseudo-random off the tick count instead. +static bool __KernelDelayReturnsAtOnce(u32 usec) { + const PSPThread *thread = __GetCurrentThread(); + const int gapPercent = thread && !thread->hasWaited ? 165 : 60; + const int chancePercent = gapPercent - (int)std::min(usec, 2U) * 100; + if (chancePercent <= 0) { + return false; + } + u64 x = (u64)CoreTiming::GetTicks(currentMIPS) * 0x9E3779B97F4A7C15ULL; + x ^= x >> 29; + return (int)(x % 100) < chancePercent; +} + int sceKernelDelayThreadCB(u32 usec) { hleEatCycles(2000); - // Note: Sometimes (0) won't delay, potentially based on how much the thread is doing. - // But a loop with just 0 often does delay, and games depend on this. So we err on that side. + if (__KernelDelayReturnsAtOnce(usec) && !__KernelCurHasReadyCallbacks()) { + return hleLogDebug(Log::sceKernel, 0, "deadline already passed"); + } SceUID curThread = __KernelGetCurThread(); s64 delayUs = __KernelDelayThreadUs(usec); __KernelScheduleWakeup(curThread, delayUs); @@ -2619,8 +2646,9 @@ int sceKernelDelayThreadCB(u32 usec) { int sceKernelDelayThread(u32 usec) { hleEatCycles(2000); - // Note: Sometimes (0) won't delay, potentially based on how much the thread is doing. - // But a loop with just 0 often does delay, and games depend on this. So we err on that side. + if (__KernelDelayReturnsAtOnce(usec)) { + return hleLogDebug(Log::sceKernel, 0, "deadline already passed"); + } SceUID curThread = __KernelGetCurThread(); s64 delayUs = __KernelDelayThreadUs(usec); __KernelScheduleWakeup(curThread, delayUs); diff --git a/Core/HLE/sceKernelThread.h b/Core/HLE/sceKernelThread.h index 75971a31ee..34ea7f6a44 100644 --- a/Core/HLE/sceKernelThread.h +++ b/Core/HLE/sceKernelThread.h @@ -305,6 +305,8 @@ public: KernelThreadDebugInterface debug; bool isProcessingCallbacks = false; + // False until the thread first waits after being started (see __KernelDelayReturnsAtOnce). + bool hasWaited = true; u32 currentMipscallId = -1; SceUID currentCallbackId = -1; diff --git a/pspautotests b/pspautotests index 4a0982f174..73755a0066 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit 4a0982f174f476dec6a918773c76f36cd1794ada +Subproject commit 73755a0066b993e7eb95145e148bdd125d847b07 diff --git a/test.py b/test.py index e37a480795..40a61f0409 100755 --- a/test.py +++ b/test.py @@ -389,6 +389,7 @@ tests_good = [ "threads/mutex/unlock", "threads/mutex/unlock2", "threads/scheduling/dispatch", + "threads/scheduling/delayzero", "threads/semaphores/semaphores", "threads/semaphores/cancel", "threads/semaphores/create", @@ -425,6 +426,7 @@ tests_good = [ "threads/tls/refer", "threads/vpl/allocate", "threads/vpl/cancel", + "threads/vpl/create", "threads/vpl/delete", "threads/vpl/fifo", "threads/vpl/free", @@ -602,7 +604,6 @@ tests_next = [ "threads/scheduling/scheduling", "threads/threads/create", "threads/tls/memory", - "threads/vpl/create", "umd/io/umd_io", "umd/raw_access/raw_access", "utility/msgdialog/dialog",