Threads: Wait timeouts work like hardware's, one rule for all of them

Every wait with a timeout behaves the same on hardware (pspautotests
threads/scheduling/waittimeouts). The deadline is taken, and the alarm set
up a moment later. If the deadline has passed by then, the wait fails with
WAIT_TIMEOUT at once, without yielding or writing the timeout back. That's
usual for 0us, half the time for 1us, and rare after; AllocateVpl does more
first. Otherwise it ends max(t, 205us) + ~35us after the call. Each object
had its own guess (24/245, 25/250, 20/250 and so on), and only MsgPipe had
the immediate case.

__KernelWaitTimesOutAtOnce() and __KernelWaitTimeoutUs() now do it for
semaphores, event flags, mutexes, lwmutexes, mbx, msgpipes, fpl, vpl and
WaitThreadEnd. The latency past the deadline isn't counted in the time
left written back.

Outcomes that hardware decides by the clock's phase (these, and
sceKernelDelayThread returning at once) go with the likelier one. Ones
between 50% and certain are instead spread evenly over calls, so a polling
loop can't lock into never yielding (sceKernelThread section version 7).
This replaces the pseudo-random choice for delays.

Also adds threads/scheduling/readyqueue, which already passes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-09-30 09:06:30 -06:00
1 parent f731e45f40
commit fb9ac8397e
10 files changed
+116 -98

No files matched your search

+12 -16
View File
@@ -434,16 +434,10 @@ static void __KernelWaitMutex(PSPMutex *mutex, u32 timeoutPtr) {
if (timeoutPtr == 0 || mutexWaitTimer == -1)
return;
int micro = (int) Memory::ReadUnchecked_U32(timeoutPtr);
// This happens to be how the hardware seems to time things.
if (micro <= 3)
micro = 25;
else if (micro <= 249)
micro = 250;
u32 micro = Memory::ReadUnchecked_U32(timeoutPtr);
// This should call __KernelMutexTimeout() later, unless we cancel it.
CoreTiming::ScheduleEvent(usToCycles(micro), mutexWaitTimer, __KernelGetCurThread());
CoreTiming::ScheduleEvent(usToCycles(__KernelWaitTimeoutUs(micro)), mutexWaitTimer, __KernelGetCurThread());
}
int sceKernelCancelMutex(SceUID uid, int count, u32 numWaitThreadsPtr) {
@@ -513,6 +507,8 @@ int sceKernelLockMutex(SceUID id, int count, u32 timeoutPtr) {
}
}
if (__KernelWaitTimesOutAtOnce(timeoutPtr))
return hleLogDebug(Log::sceKernel, SCE_KERNEL_ERROR_WAIT_TIMEOUT, "timed out at once");
SceUID threadID = __KernelGetCurThread();
// May be in a tight loop timing out (where we don't remove from waitingThreads yet), don't want to add duplicates.
if (std::find(mutex->waitingThreads.begin(), mutex->waitingThreads.end(), threadID) == mutex->waitingThreads.end())
@@ -533,6 +529,8 @@ int sceKernelLockMutexCB(SceUID id, int count, u32 timeoutPtr) {
if (error)
return hleLogError(Log::sceKernel, error);
if (__KernelWaitTimesOutAtOnce(timeoutPtr))
return hleLogDebug(Log::sceKernel, SCE_KERNEL_ERROR_WAIT_TIMEOUT, "timed out at once");
SceUID threadID = __KernelGetCurThread();
// May be in a tight loop timing out (where we don't remove from waitingThreads yet), don't want to add duplicates.
if (std::find(mutex->waitingThreads.begin(), mutex->waitingThreads.end(), threadID) == mutex->waitingThreads.end())
@@ -812,16 +810,10 @@ static void __KernelWaitLwMutex(LwMutex *mutex, u32 timeoutPtr) {
if (timeoutPtr == 0 || lwMutexWaitTimer == -1)
return;
int micro = (int) Memory::ReadUnchecked_U32(timeoutPtr);
// This happens to be how the hardware seems to time things.
if (micro <= 3)
micro = 25;
else if (micro <= 249)
micro = 250;
u32 micro = Memory::ReadUnchecked_U32(timeoutPtr);
// This should call __KernelLwMutexTimeout() later, unless we cancel it.
CoreTiming::ScheduleEvent(usToCycles(micro), lwMutexWaitTimer, __KernelGetCurThread());
CoreTiming::ScheduleEvent(usToCycles(__KernelWaitTimeoutUs(micro)), lwMutexWaitTimer, __KernelGetCurThread());
}
static bool __KernelUnlockLwMutexForThreadCheck(LwMutex *mutex, SceUID threadID, u32 &error, int result, bool &wokeThreads)
@@ -901,6 +893,8 @@ int sceKernelLockLwMutex(u32 workareaPtr, int count, u32 timeoutPtr) {
if (!mutex) {
return hleLogError(Log::sceKernel, error);
} else {
if (__KernelWaitTimesOutAtOnce(timeoutPtr))
return hleLogDebug(Log::sceKernel, SCE_KERNEL_ERROR_WAIT_TIMEOUT, "timed out at once");
SceUID threadID = __KernelGetCurThread();
// May be in a tight loop timing out (where we don't remove from waitingThreads yet), don't want to add duplicates.
if (std::find(mutex->waitingThreads.begin(), mutex->waitingThreads.end(), threadID) == mutex->waitingThreads.end())
@@ -932,6 +926,8 @@ int sceKernelLockLwMutexCB(u32 workareaPtr, int count, u32 timeoutPtr) {
if (!mutex) {
return hleLogError(Log::sceKernel, error);
} else {
if (__KernelWaitTimesOutAtOnce(timeoutPtr))
return hleLogDebug(Log::sceKernel, SCE_KERNEL_ERROR_WAIT_TIMEOUT, "timed out at once");
SceUID threadID = __KernelGetCurThread();
// May be in a tight loop timing out (where we don't remove from waitingThreads yet), don't want to add duplicates.
if (std::find(mutex->waitingThreads.begin(), mutex->waitingThreads.end(), threadID) == mutex->waitingThreads.end())