From fb9ac8397ea16b49ba9f0230896aa20e7e01ca5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 29 Sep 2026 11:36:22 -0600 Subject: [PATCH] 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) --- Core/HLE/KernelWaitHelpers.h | 4 +- Core/HLE/sceKernelEventFlag.cpp | 16 +++---- Core/HLE/sceKernelMbx.cpp | 16 +++---- Core/HLE/sceKernelMemory.cpp | 43 ++++++------------- Core/HLE/sceKernelMsgPipe.cpp | 11 ++--- Core/HLE/sceKernelMutex.cpp | 28 ++++++------ Core/HLE/sceKernelSemaphore.cpp | 13 ++---- Core/HLE/sceKernelThread.cpp | 76 +++++++++++++++++++++++++-------- Core/HLE/sceKernelThread.h | 5 +++ test.py | 2 + 10 files changed, 116 insertions(+), 98 deletions(-) diff --git a/Core/HLE/KernelWaitHelpers.h b/Core/HLE/KernelWaitHelpers.h index 196ee10060..0c02b74d10 100644 --- a/Core/HLE/KernelWaitHelpers.h +++ b/Core/HLE/KernelWaitHelpers.h @@ -30,7 +30,7 @@ namespace HLEKernel { // Cancels the pending timeout event for a wait that's being satisfied, and writes the time -// remaining back to the game. +// remaining back to the game. The event goes off WAIT_TIMEOUT_LATENCY_US after the deadline. // Note the clamp: UnscheduleEvent returns the scheduled time minus the current time, which goes // negative when the event is already overdue but hasn't been processed yet. Without the clamp we'd // write a huge bogus timeout back into the game's variable. @@ -38,7 +38,7 @@ inline void WriteRemainingTimeout(int waitTimer, SceUID threadID, u32 timeoutPtr if (timeoutPtr == 0 || waitTimer == -1) return; - s64 cyclesLeft = CoreTiming::UnscheduleEvent(waitTimer, threadID); + s64 cyclesLeft = CoreTiming::UnscheduleEvent(waitTimer, threadID) - usToCycles(WAIT_TIMEOUT_LATENCY_US); if (cyclesLeft < 0) cyclesLeft = 0; Memory::WriteOrException_U32((u32)cyclesToUs(cyclesLeft), timeoutPtr); diff --git a/Core/HLE/sceKernelEventFlag.cpp b/Core/HLE/sceKernelEventFlag.cpp index 734aad266b..0fa0e6f954 100644 --- a/Core/HLE/sceKernelEventFlag.cpp +++ b/Core/HLE/sceKernelEventFlag.cpp @@ -302,16 +302,8 @@ static void __KernelSetEventFlagTimeout(EventFlag *e, u32 timeoutPtr) { if (timeoutPtr == 0 || eventFlagWaitTimer == -1) return; - int micro = (int) Memory::ReadOrException_U32(timeoutPtr); - - // This seems like the actual timing of timeouts on hardware. - if (micro <= 1) - micro = 25; - else if (micro <= 209) - micro = 240; - - // This should call __KernelEventFlagTimeout() later, unless we cancel it. - CoreTiming::ScheduleEvent(usToCycles(micro), eventFlagWaitTimer, __KernelGetCurThread()); + u32 micro = Memory::ReadOrException_U32(timeoutPtr); + CoreTiming::ScheduleEvent(usToCycles(__KernelWaitTimeoutUs(micro)), eventFlagWaitTimer, __KernelGetCurThread()); } int sceKernelWaitEventFlag(SceUID id, u32 bits, u32 wait, u32 outBitsPtr, u32 timeoutPtr) { @@ -345,6 +337,8 @@ int sceKernelWaitEventFlag(SceUID id, u32 bits, u32 wait, u32 outBitsPtr, u32 ti return hleLogDebug(Log::sceKernel, SCE_KERNEL_ERROR_EVF_MULTI); } + if (__KernelWaitTimesOutAtOnce(timeoutPtr)) + return hleLogDebug(Log::sceKernel, SCE_KERNEL_ERROR_WAIT_TIMEOUT, "timed out at once"); (void)hleLogDebug(Log::sceKernel, 0, "waiting"); // No match - must wait. @@ -408,6 +402,8 @@ int sceKernelWaitEventFlagCB(SceUID id, u32 bits, u32 wait, u32 outBitsPtr, u32 return hleLogDebug(Log::sceKernel, SCE_KERNEL_ERROR_EVF_MULTI); } + if (!doCallbackWait && __KernelWaitTimesOutAtOnce(timeoutPtr)) + return hleLogDebug(Log::sceKernel, SCE_KERNEL_ERROR_WAIT_TIMEOUT, "timed out at once"); (void)hleLogDebug(Log::sceKernel, 0, "waiting"); // No match - must wait. diff --git a/Core/HLE/sceKernelMbx.cpp b/Core/HLE/sceKernelMbx.cpp index bd52ccf06c..51f6782cac 100644 --- a/Core/HLE/sceKernelMbx.cpp +++ b/Core/HLE/sceKernelMbx.cpp @@ -253,16 +253,8 @@ static void __KernelWaitMbx(Mbx *m, u32 timeoutPtr) if (timeoutPtr == 0 || mbxWaitTimer == -1) return; - int micro = (int) Memory::ReadOrException_U32(timeoutPtr); - - // This seems to match the actual timing. - if (micro <= 2) - micro = 20; - else if (micro <= 209) - micro = 250; - - // This should call __KernelMbxTimeout() later, unless we cancel it. - CoreTiming::ScheduleEvent(usToCycles(micro), mbxWaitTimer, __KernelGetCurThread()); + u32 micro = Memory::ReadOrException_U32(timeoutPtr); + CoreTiming::ScheduleEvent(usToCycles(__KernelWaitTimeoutUs(micro)), mbxWaitTimer, __KernelGetCurThread()); } static std::vector::iterator __KernelMbxFindPriority(std::vector &waiting) @@ -441,6 +433,8 @@ int sceKernelReceiveMbx(SceUID id, u32 packetAddrPtr, u32 timeoutPtr) { return hleLogDebug(Log::sceKernel, m->ReceiveMessage(packetAddrPtr), "sending first queue message"); } else { HLEKernel::RemoveWaitingThread(m->waitingThreads, __KernelGetCurThread()); + if (__KernelWaitTimesOutAtOnce(timeoutPtr)) + return hleLogDebug(Log::sceKernel, SCE_KERNEL_ERROR_WAIT_TIMEOUT, "timed out at once"); m->AddWaitingThread(__KernelGetCurThread(), packetAddrPtr); __KernelWaitMbx(m, timeoutPtr); __KernelWaitCurThread(WAITTYPE_MBX, id, 0, timeoutPtr, false, "mbx waited"); @@ -460,6 +454,8 @@ int sceKernelReceiveMbxCB(SceUID id, u32 packetAddrPtr, u32 timeoutPtr) { return hleLogDebug(Log::sceKernel, m->ReceiveMessage(packetAddrPtr), "sending first queue message"); } else { HLEKernel::RemoveWaitingThread(m->waitingThreads, __KernelGetCurThread()); + if (__KernelWaitTimesOutAtOnce(timeoutPtr)) + return hleLogDebug(Log::sceKernel, SCE_KERNEL_ERROR_WAIT_TIMEOUT, "timed out at once"); m->AddWaitingThread(__KernelGetCurThread(), packetAddrPtr); __KernelWaitMbx(m, timeoutPtr); __KernelWaitCurThread(WAITTYPE_MBX, id, 0, timeoutPtr, true, "mbx waited"); diff --git a/Core/HLE/sceKernelMemory.cpp b/Core/HLE/sceKernelMemory.cpp index 136096053a..2eaa43ba37 100644 --- a/Core/HLE/sceKernelMemory.cpp +++ b/Core/HLE/sceKernelMemory.cpp @@ -641,19 +641,8 @@ static void __KernelSetFplTimeout(u32 timeoutPtr) if (timeoutPtr == 0 || fplWaitTimer == -1) return; - int micro = (int) Memory::ReadOrException_U32(timeoutPtr); - - // TODO: test for fpls. - // This happens to be how the hardware seems to time things. - if (micro <= 5) - micro = 20; - // Yes, this 7 is reproducible. 6 is (a lot) longer than 7. - else if (micro == 7) - micro = 25; - else if (micro <= 215) - micro = 250; - - CoreTiming::ScheduleEvent(usToCycles(micro), fplWaitTimer, __KernelGetCurThread()); + u32 micro = Memory::ReadOrException_U32(timeoutPtr); + CoreTiming::ScheduleEvent(usToCycles(__KernelWaitTimeoutUs(micro)), fplWaitTimer, __KernelGetCurThread()); } int sceKernelAllocateFpl(SceUID uid, u32 blockPtrAddr, u32 timeoutPtr) { @@ -671,6 +660,8 @@ int sceKernelAllocateFpl(SceUID uid, u32 blockPtrAddr, u32 timeoutPtr) { } else { SceUID threadID = __KernelGetCurThread(); HLEKernel::RemoveWaitingThread(fpl->waitingThreads, threadID); + if (__KernelWaitTimesOutAtOnce(timeoutPtr)) + return hleLogDebug(Log::sceKernel, SCE_KERNEL_ERROR_WAIT_TIMEOUT, "timed out at once"); FplWaitingThread waiting = {threadID, blockPtrAddr}; fpl->waitingThreads.push_back(waiting); @@ -696,6 +687,8 @@ int sceKernelAllocateFplCB(SceUID uid, u32 blockPtrAddr, u32 timeoutPtr) { } else { SceUID threadID = __KernelGetCurThread(); HLEKernel::RemoveWaitingThread(fpl->waitingThreads, threadID); + if (__KernelWaitTimesOutAtOnce(timeoutPtr)) + return hleLogDebug(Log::sceKernel, SCE_KERNEL_ERROR_WAIT_TIMEOUT, "timed out at once"); FplWaitingThread waiting = {threadID, blockPtrAddr}; fpl->waitingThreads.push_back(waiting); @@ -1492,18 +1485,8 @@ static void __KernelSetVplTimeout(u32 timeoutPtr) if (timeoutPtr == 0 || vplWaitTimer == -1) return; - int micro = (int) Memory::ReadOrException_U32(timeoutPtr); - - // This happens to be how the hardware seems to time things. - if (micro <= 5) - micro = 20; - // Yes, this 7 is reproducible. 6 is (a lot) longer than 7. - else if (micro == 7) - micro = 25; - else if (micro <= 215) - micro = 250; - - CoreTiming::ScheduleEvent(usToCycles(micro), vplWaitTimer, __KernelGetCurThread()); + u32 micro = Memory::ReadOrException_U32(timeoutPtr); + CoreTiming::ScheduleEvent(usToCycles(__KernelWaitTimeoutUs(micro)), vplWaitTimer, __KernelGetCurThread()); } int sceKernelAllocateVpl(SceUID uid, u32 size, u32 addrPtr, u32 timeoutPtr) @@ -1514,8 +1497,9 @@ int sceKernelAllocateVpl(SceUID uid, u32 size, u32 addrPtr, u32 timeoutPtr) VPL *vpl = kernelObjects.Get(uid, ignore); if (error == SCE_KERNEL_ERROR_NO_MEMORY) { - if (timeoutPtr != 0 && Memory::ReadOrException_U32(timeoutPtr) == 0) - return hleLogError(Log::sceKernel, SCE_KERNEL_ERROR_WAIT_TIMEOUT); + // Allocating does more before setting up the timeout, so short ones time out at once more often. + if (__KernelWaitTimesOutAtOnce(timeoutPtr, 117, 17)) + return hleLogDebug(Log::sceKernel, SCE_KERNEL_ERROR_WAIT_TIMEOUT, "timed out at once"); if (vpl) { SceUID threadID = __KernelGetCurThread(); @@ -1544,8 +1528,9 @@ int sceKernelAllocateVplCB(SceUID uid, u32 size, u32 addrPtr, u32 timeoutPtr) VPL *vpl = kernelObjects.Get(uid, ignore); if (error == SCE_KERNEL_ERROR_NO_MEMORY) { - if (timeoutPtr != 0 && Memory::ReadOrException_U32(timeoutPtr) == 0) - return hleLogError(Log::sceKernel, SCE_KERNEL_ERROR_WAIT_TIMEOUT); + // Allocating does more before setting up the timeout, so short ones time out at once more often. + if (__KernelWaitTimesOutAtOnce(timeoutPtr, 117, 17)) + return hleLogDebug(Log::sceKernel, SCE_KERNEL_ERROR_WAIT_TIMEOUT, "timed out at once"); if (vpl) { diff --git a/Core/HLE/sceKernelMsgPipe.cpp b/Core/HLE/sceKernelMsgPipe.cpp index 82a2f2108d..f045a20718 100644 --- a/Core/HLE/sceKernelMsgPipe.cpp +++ b/Core/HLE/sceKernelMsgPipe.cpp @@ -269,15 +269,16 @@ static bool __KernelSetMsgPipeTimeout(u32 timeoutPtr) { if (timeoutPtr == 0 || waitTimer == -1) return true; - int micro = (int)Memory::ReadOrException_U32(timeoutPtr); - if (micro <= 2) { + // Always at once up to 2us, never from 3us: what threads/msgpipe/send and receive record, where + // each call comes straight after a checkpoint and the code is presumably cold. Back to back the + // hardware does 2us at once less often, as other waits do. + if (__KernelWaitTimesOutAtOnce(timeoutPtr, 210, 55)) { // Don't wait or reschedule, just timeout immediately. return false; } - if (micro <= 210) - micro = 250; - CoreTiming::ScheduleEvent(usToCycles(micro), waitTimer, __KernelGetCurThread()); + const u32 micro = Memory::ReadOrException_U32(timeoutPtr); + CoreTiming::ScheduleEvent(usToCycles(__KernelWaitTimeoutUs(micro)), waitTimer, __KernelGetCurThread()); return true; } diff --git a/Core/HLE/sceKernelMutex.cpp b/Core/HLE/sceKernelMutex.cpp index da149aebba..26367bc09d 100644 --- a/Core/HLE/sceKernelMutex.cpp +++ b/Core/HLE/sceKernelMutex.cpp @@ -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()) diff --git a/Core/HLE/sceKernelSemaphore.cpp b/Core/HLE/sceKernelSemaphore.cpp index 41d43aea7b..b4278a7d05 100644 --- a/Core/HLE/sceKernelSemaphore.cpp +++ b/Core/HLE/sceKernelSemaphore.cpp @@ -294,16 +294,9 @@ static void __KernelSetSemaTimeout(PSPSemaphore *s, u32 timeoutPtr) { if (timeoutPtr == 0 || semaWaitTimer == -1) return; - int micro = (int)Memory::ReadOrException_U32(timeoutPtr); - - // This happens to be how the hardware seems to time things. - if (micro <= 3) - micro = 24; - else if (micro <= 249) - micro = 245; - + u32 micro = Memory::ReadOrException_U32(timeoutPtr); // This should call __KernelSemaTimeout() later, unless we cancel it. - CoreTiming::ScheduleEvent(usToCycles(micro), semaWaitTimer, __KernelGetCurThread()); + CoreTiming::ScheduleEvent(usToCycles(__KernelWaitTimeoutUs(micro)), semaWaitTimer, __KernelGetCurThread()); } static int __KernelWaitSema(SceUID id, int wantedCount, u32 timeoutPtr, bool processCallbacks) { @@ -327,6 +320,8 @@ static int __KernelWaitSema(SceUID id, int wantedCount, u32 timeoutPtr, bool pro if (s->ns.currentCount >= wantedCount && s->waitingThreads.size() == 0 && !hasCallbacks) { s->ns.currentCount -= wantedCount; } else { + if (!hasCallbacks && __KernelWaitTimesOutAtOnce(timeoutPtr)) + return SCE_KERNEL_ERROR_WAIT_TIMEOUT; 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(s->waitingThreads.begin(), s->waitingThreads.end(), threadID) == s->waitingThreads.end()) diff --git a/Core/HLE/sceKernelThread.cpp b/Core/HLE/sceKernelThread.cpp index 085581b450..d25f743598 100644 --- a/Core/HLE/sceKernelThread.cpp +++ b/Core/HLE/sceKernelThread.cpp @@ -496,6 +496,8 @@ struct BusySyscall { bool counting; }; static std::vector busySyscalls; +// See __KernelChance(). +static int g_chanceAccumulator = 50; static int eventBusySyscallDone = -1; static void __KernelBusySyscallDone(u64 userdata, int cyclesLate); static SceUID currentThread; @@ -707,6 +709,7 @@ static bool __KernelCheckResumeThreadEnd(PSPThread *t, SceUID waitingThreadID, u u32 timeoutPtr = __KernelGetWaitTimeoutPtr(waitingThreadID, error); // Note: unlike the helper in KernelWaitHelpers.h, we unschedule even without a timeout ptr. s64 cyclesLeft = CoreTiming::UnscheduleEvent(eventThreadEndTimeout, waitingThreadID); + cyclesLeft -= usToCycles(WAIT_TIMEOUT_LATENCY_US); if (cyclesLeft < 0) cyclesLeft = 0; if (timeoutPtr != 0) @@ -771,6 +774,7 @@ u32 HLEMipsCallReturnAddress() { } void __KernelThreadingInit() { + g_chanceAccumulator = 50; struct ThreadHack { u32 nid; u32 *addr; @@ -844,7 +848,7 @@ void __KernelThreadingDoState(PointerWrap &p) g_exitCallbackPending = false; } - auto s = p.Section("sceKernelThread", 1, 6); + auto s = p.Section("sceKernelThread", 1, 7); if (!s) return; @@ -906,6 +910,11 @@ void __KernelThreadingDoState(PointerWrap &p) busySyscalls.clear(); } CoreTiming::RestoreRegisterEvent(eventBusySyscallDone, "BusySyscallDone", &__KernelBusySyscallDone); + if (s >= 7) { + Do(p, g_chanceAccumulator); + } else { + g_chanceAccumulator = 50; + } __SetCurrentThread(kernelObjects.GetFast(currentThread), currentThread, __KernelGetThreadName(currentThread)); lastSwitchCycles = CoreTiming::GetTicks(currentMIPS); @@ -1503,6 +1512,42 @@ void __KernelCancelWakeup(SceUID threadID) CoreTiming::UnscheduleEvent(eventScheduledWakeup, threadID); } +// On hardware these outcomes depend on the phase of the clock. We go with the likelier one, except +// that always taking a likely-but-not-certain one could lock a polling loop into never yielding. +// Those are true for the given share of calls, spread evenly: 60 is true three times in five. +static bool __KernelChance(int percent) { + if (percent < 50) { + return false; + } else if (percent >= 100) { + return true; + } + g_chanceAccumulator += percent; + if (g_chanceAccumulator >= 100) { + g_chanceAccumulator -= 100; + return true; + } + return false; +} + +// How a wait's timeout plays out 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 SCE_KERNEL_ERROR_WAIT_TIMEOUT at once, without giving up the CPU or writing the +// timeout back. For most calls that happens about 85% of the time for 0us, half the time for 1us, +// 15% for 2us and never from 3us on. sceKernelAllocateVpl does more first: always up to 1us, then +// about 17% less per us. Longer ones end max(timeout, 205us) plus about 35us after the call, the +// last few of which we already spend around the event. +bool __KernelWaitTimesOutAtOnce(u32 timeoutPtr, int basePercent, int stepPercent) { + if (!Memory::IsValid4AlignedAddress(timeoutPtr)) { + return false; + } + const u32 micro = std::min(Memory::ReadUnchecked_U32(timeoutPtr), 100U); + return __KernelChance(basePercent - (int)micro * stepPercent); +} + +s64 __KernelWaitTimeoutUs(u32 micro) { + return (s64)std::max(micro, 205U) + WAIT_TIMEOUT_LATENCY_US; +} + void hleThreadEndTimeout(u64 userdata, int cyclesLate) { SceUID threadID = (SceUID) userdata; @@ -1546,7 +1591,7 @@ void __KernelStopThread(SceUID threadID, int exitStatus, const char *reason) u32 timeoutPtr = __KernelGetWaitTimeoutPtr(waitingThread, error); if (HLEKernel::VerifyWait(waitingThread, WAITTYPE_THREADEND, threadID)) { - s64 cyclesLeft = CoreTiming::UnscheduleEvent(eventThreadEndTimeout, waitingThread); + s64 cyclesLeft = CoreTiming::UnscheduleEvent(eventThreadEndTimeout, waitingThread) - usToCycles(WAIT_TIMEOUT_LATENCY_US); if (cyclesLeft < 0) cyclesLeft = 0; if (timeoutPtr != 0) @@ -2614,22 +2659,15 @@ static s64 __KernelDelayThreadUs(u64 usec) { } // 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. +// 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). 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; + return __KernelChance(gapPercent - (int)std::min(usec, 2U) * 100); } int sceKernelDelayThreadCB(u32 usec) { @@ -2796,8 +2834,10 @@ int sceKernelWaitThreadEnd(SceUID threadID, u32 timeoutPtr) { } else { if (t->nt.status != THREADSTATUS_DORMANT) { + if (__KernelWaitTimesOutAtOnce(timeoutPtr)) + return hleLogDebug(Log::sceKernel, SCE_KERNEL_ERROR_WAIT_TIMEOUT, "timed out at once"); if (Memory::IsValidAddress(timeoutPtr)) - __KernelScheduleThreadEndTimeout(currentThread, threadID, Memory::ReadUnchecked_U32(timeoutPtr)); + __KernelScheduleThreadEndTimeout(currentThread, threadID, __KernelWaitTimeoutUs(Memory::ReadUnchecked_U32(timeoutPtr))); if (std::find(t->waitingThreads.begin(), t->waitingThreads.end(), currentThread) == t->waitingThreads.end()) t->waitingThreads.push_back(currentThread); __KernelWaitCurThread(WAITTYPE_THREADEND, threadID, 0, timeoutPtr, false, "thread wait end"); @@ -2823,8 +2863,10 @@ int sceKernelWaitThreadEndCB(SceUID threadID, u32 timeoutPtr) { } else { if (t->nt.status != THREADSTATUS_DORMANT) { + if (__KernelWaitTimesOutAtOnce(timeoutPtr)) + return hleLogDebug(Log::sceKernel, SCE_KERNEL_ERROR_WAIT_TIMEOUT, "timed out at once"); if (Memory::IsValidAddress(timeoutPtr)) - __KernelScheduleThreadEndTimeout(currentThread, threadID, Memory::ReadUnchecked_U32(timeoutPtr)); + __KernelScheduleThreadEndTimeout(currentThread, threadID, __KernelWaitTimeoutUs(Memory::ReadUnchecked_U32(timeoutPtr))); if (std::find(t->waitingThreads.begin(), t->waitingThreads.end(), currentThread) == t->waitingThreads.end()) t->waitingThreads.push_back(currentThread); __KernelWaitCurThread(WAITTYPE_THREADEND, threadID, 0, timeoutPtr, true, "thread wait end"); diff --git a/Core/HLE/sceKernelThread.h b/Core/HLE/sceKernelThread.h index 34ea7f6a44..3d498717f0 100644 --- a/Core/HLE/sceKernelThread.h +++ b/Core/HLE/sceKernelThread.h @@ -379,6 +379,11 @@ u32 __KernelGetWaitTimeoutPtr(SceUID threadID, u32 &error); SceUID __KernelGetWaitID(SceUID threadID, WaitType type, u32 &error); SceUID __KernelGetCurrentCallbackID(SceUID threadID, u32 &error); void __KernelWaitCurThread(WaitType type, SceUID waitId, u32 waitValue, u32 timeoutPtr, bool processCallbacks, const char *reason); +// See the definition for how hardware times waits out. +bool __KernelWaitTimesOutAtOnce(u32 timeoutPtr, int basePercent = 85, int stepPercent = 35); +s64 __KernelWaitTimeoutUs(u32 micro); +// How long after its deadline a wait's timeout goes off. Not part of the time left written back. +const int WAIT_TIMEOUT_LATENCY_US = 30; void __KernelWaitCallbacksCurThread(WaitType type, SceUID waitID, u32 waitValue, u32 timeoutPtr); void __KernelReSchedule(const char *reason = "no reason"); void __KernelReSchedule(bool doCallbacks, const char *reason); diff --git a/test.py b/test.py index a63ea7ca58..5e18a2e8a5 100755 --- a/test.py +++ b/test.py @@ -393,6 +393,8 @@ tests_good = [ "threads/mutex/unlock2", "threads/scheduling/dispatch", "threads/scheduling/delayzero", + "threads/scheduling/readyqueue", + "threads/scheduling/waittimeouts", "threads/semaphores/semaphores", "threads/semaphores/cancel", "threads/semaphores/create",