Repository navigation
Description
On Windows, the std:: timed waits (condition_variable::wait_for and wait_until, this_thread::sleep_for and sleep_until) go through winpthreads. winpthreads ends a timed wait only once GetTickCount64() has advanced by the timeout. GetTickCount64 moves in 15.6 ms steps whatever the timer resolution, so the 0.5 ms tick Time.cpp sets at load does not shorten these waits.
Measured
Windows 10 (19045), MinGW-w64 GCC 15.2.0 (WinLibs r7), with the tick raised as Time.cpp does (NtSetTimerResolution(5000)), 40 calls each:
| Call | Median |
|---|---|
GetTickCount64 step |
15.5 ms |
condition_variable::wait_for(1 ms) |
15.5 ms |
this_thread::sleep_for(1 ms) |
15.6 ms |
this_thread::sleep_for(2 ms) |
15.5 ms |
condition_variable::wait_until(now + 16.68 ms) |
returns 14.5 ms after the deadline (12.6 to 16.2) |
this_thread::sleep_until(now + 16.68 ms) |
30.4 ms |
this_thread::sleep_for(50 µs) |
0.000 ms |
Sleep(1), WaitForSingleObject(event, 1), SleepConditionVariableSRW(1) |
1.1 to 1.2 ms |
| High-resolution waitable timer, 1 ms | 1.5 ms |
Why
In mingw-w64's winpthreads/src/misc.c, _pthread_wait_for_single_object calls WaitForSingleObject again until GetTickCount64() reaches start + timeout. pthread_cond_timedwait (cond.c) and nanosleep (through pthread_delay_np_ms) wait through it, and libstdc++'s condition_variable and this_thread::sleep_* call those. nanosleep also converts the request to whole milliseconds (tv_nsec / POW10_6). A request under 1 ms therefore returns without waiting or yielding.
The comment above g_timerResolutionRaised in Time.cpp lists winpthreads nanosleep and std::condition_variable::wait_for among the waits the 0.5 ms tick fixes. The tick does fix Sleep and WaitFor*Object, but not these two. Guest waits are not affected, because they go through TimedWait and its high-resolution timers. PollSleep in WaitMemory.cpp and PreciseSleepUs in libc/include/PreciseWait.hpp also use high-resolution timers.
Host waits that still go through winpthreads
Each of these timed waits can end up to about 15.6 ms late on Windows:
VideoOutDriver::vblankLoop:wait_untilthe next vblank. Over 3 × 240 vblanks, intervals ranged from 2 ms to 34 ms instead of 16.68 ms. Fixed by fix(videoout): pace vblanks with a high-resolution timer #1472.QueueWorker.cpp:69: an idle queue worker with completions pending reaps after eachwait_for(1 ms). On Windows that is about every 15.6 ms, so deferred end-of-pipe interrupts reached the guest about 15 ms after the GPU finished. Fixed by fix(agc): poll for completions with a high-resolution timer on Windows #1474.QueueWorker.cpp:83: between notifications, an order release is re-checked after eachwait_for(1 ms). Also changed in fix(agc): poll for completions with a high-resolution timer on Windows #1474.AudioOut2Context.cpp:327: a blockingsceAudioOut2ContextPushon a full queue checks again after eachsleep_for(1 ms). Fixed by fix(audio): pace blocking audio calls with a high-resolution timer #1486.sceAudio3dPortPush(blocking):sleep_untilthe queue has room. Fixed by fix(audio): pace blocking audio calls with a high-resolution timer #1486, along withsceAudioOutOutput, which paces itself withnanosleepand is not in this list.libSceAudioIncapturepolls the device after eachsleep_for(1 ms). With no device,sceAudioInInputwaits for its next block withsleep_until. Fixed by fix(audio): pace blocking audio calls with a high-resolution timer #1486.Player::controllerLoop(libSceAvPlayer): checks for the end of the file after eachwait_for(5 ms).VideoOutDriver::presentLoop: while no flip is queued, SDL events and the pad are polled after eachwait_for(10 ms).libSceNetepoll wait with no sockets registered:wait_until(min(deadline, now + 100 ms)).
These polls use a sleep_for under 1 ms, which returns at once, so they spin without yielding:
Pm4.cpp:814, WAIT_REG_MEM (50 µs)Apr.cpp:588(50 µs)Aio.cpp:280and:305(10 µs)libSceFontState.cpp:59(30 µs)
I can send PRs for these areas separately.
Activity
klosixgaming-arch commentedon Oct 8, 2026
This issue is related to several recent PRs I've reviewed that address Windows timing issues:
sceKernelRaiseExceptionhandlersThe comprehensive analysis of winpthreads' use of
GetTickCount64()is valuable. The measured data showing 15.6 ms steps despite the 0.5 ms tick resolution clearly demonstrates that the tick setting doesn't affect these waits.Based on my review of related PRs, I'd suggest:
TimedWaitandPreciseSleepUs) is the correct long-term solutionThis issue provides excellent documentation for anyone working on Windows timing improvements in the future.
popcoder565 commentedon Oct 9, 2026
Status of the affected call sites, for anyone picking this up (
main6cc0503):PreciseSleepUs.selectandpollwith no sockets sleep throughstd::this_thread::sleep_forin whole milliseconds (select_nid_postfixinSocket.cpp, andpollinUdp.cppwhen no descriptor is a socket). On Windowssleep_for(1 ms)takes about 15.5 ms here, soselect(0, NULL, NULL, NULL, &tv)with a 1 mstvsleeps about 15 times as long as asked. fix(libkernel): select descriptors that are not sockets #1706 and fix(libkernel): poll files, pipes and stdio descriptors #1832 change those two functions, so a fix should follow them.🤖 Addressed by Claude Code
adriadam10 commentedon Oct 10, 2026
Fixed in part by #1472 (vblank pacing), #1474 (AGC completion poll), #1486 (blocking audio calls) and #1588 (empty sceNetEpollWait) with a high-resolution timer; still open: the other
std::timed waits and sub-ms sleeps (#1576, #1655, #1780 open;selectandpollwithout sockets).justdoston commentedon Oct 10, 2026
I'd like to take the remaining
select/pollgap from @popcoder565's list: with no sockets they sleep throughstd::this_thread::sleep_forin whole milliseconds (select_nid_postfixinSocket.cpp,pollinUdp.cpp). #1706 and #1832, which a fix was going to follow, are both closed now. I'll post the branch here when it's ready.AI-assisted: yes (Claude Code).
MarcZX10 commentedon Oct 10, 2026
Status update (2026-10-10)
Since the previous status note, #1551 has merged as commit
695e0b9. It replaces the sub-millisecond waits at the AGCWAIT_REG_MEM, APR wait, and Font lock backoff call sites withPreciseSleepUs, with a focused helper test. The ineffective AIO substitutions were removed after the Windows measurements in #1576.select/pollwhen no descriptor is a socketsleep_forbelow 1 ms in the AGC/APR/Font call sitesPRs #1486 and #1588 have also merged. This updates the earlier status comment, which still listed #1551 as open.
justdoston commentedon Oct 10, 2026
The
select/pollpart is ready: https://github.com/justdoston/AnyPS5/tree/fix/windows-select-poll-precise-sleepselectwith no sockets sleeps withPreciseSleepUntilto its deadline (at most 100 ms per round, as before) instead ofsleep_forin whole milliseconds, so it no longer rounds the timeout up to a millisecond either.pollwith no sockets sleeps withPreciseSleepUs.Both use the
PreciseWait.hpphelpers from #1486 and #1588; on Linux they still sleep throughstd::this_thread::sleep_for.guest_poll, which also runs on Windows, now times 20 empty 1 mspollcalls and 20 empty 1 msselectcalls: each set must take at least 20 ms and less than 200 ms, where the 15.6 ms steps measured above take about 310 ms.Tested: Linux full
ctest(676 of 683; the 7 failures areagc_*GPU tests specific to this machine's Vulkan driver, none in sockets), withguest_pollpassing. Not run on Windows: the files cross-compile with MinGW-w64 GCC 16, and under Wine 10.0 the new waits take about 1.2 ms per 1 ms call. Wine'sGetTickCount64doesn't move in 15.6 ms steps, so it can't show the old behaviour; the before/after rests on the Windows 10 numbers in this issue.AI-assisted: yes. An AI agent (Claude Code) did most of the work: the code and the test
popcoder565 commentedon Oct 10, 2026
@justdoston I checked your branch
fix/windows-select-poll-precise-sleep(fea3e40e) on Windows. Setup: Windows 10 (19045), MinGW-w64 GCC 15.2.0 (WinLibs r7), Release, merged ontomainca34953.Socket.cppandUdp.cppchanges,guest_poll(including the new 20 × 1 mspollandselecttiming) passes 3 of 3.main'sSocket.cppandUdp.cppput back under the same test, it aborts in 3 of 3 runs (0xc0000409), so the new timing check catches the 15.6 ms steps.agc_driver, once under-j 8; reported on fix(agc): harden host import construction and mutation waits #2779, which was in the same build).🤖 Addressed by Claude Code
fix(libkernel): use precise waits for empty poll sets
fix(libkernel): use precise waits for select and poll on pipes on Win…
feat(metal): sync upstream batch 14 and optimize Civ 7 Metal memory t…
MarcZX10 commentedon Oct 17, 2026
I checked Doston's offered branch against current
main(98c251fc6). This follow-up is already covered, so I did not open a duplicate PR:select()waits toPreciseSleepUntiland added the repeated 1 ms timing regression check inguest_select_wait.poll()and added the repeated 1 ms check inguest_poll.poll()/select()include pipes.Current
maincontains these paths and tests. The offered branch is based on an older main and itsGuestPoll.cppwould also revert tests added since then, so it should not be carried as-is. Issue #1471 still tracks other timed-wait call sites.MarcZX10 commentedon Oct 19, 2026
A remaining guest-visible path is the Dinkumware C++ condition wait in
libc/src/Threads.cpp:_Cnd_timedwait_nid_postfixusesstd::condition_variable::wait_until, which reaches the winpthreads timeout path described above. The existing focused test checked only that the wait does not return early, so it did not catch the reported late timeout.I opened PR #7735 for this libc path. On Windows it uses the native condition variable with
CRITICAL_SECTION; POSIX keeps the current implementation. The added Windows regression measures repeated 1 ms waits. The PR covers this call path only and does not close the broader issue.adriadam10 commentedon Oct 20, 2026
#7735 moves the Dinkumware
_Cnd_wait/_Cnd_timedwaitpath to native Windows condition variables, covered byguest_libc_cnd_timed_wait; the otherstd::timed waits are still open.hyvvs commentedon Oct 21, 2026
I'm investigating another Windows timing callsite in
libSceUsbd::sceUsbdHandleEventsTimeout. When the USB context is absent, it usesstd::this_thread::sleep_for, which appears to have the timing behavior described here.I have a local patch using the existing
PreciseSleepUshelper and a regression test. Preliminary Windows timing measurements and C++ syntax checks look good, but I still need to run the full library tests.I currently have #8729 open, so I can't submit a second PR yet under the contributor limit. Posting here to coordinate and avoid duplicate work.
LeandroLP23 commentedon Oct 26, 2026
Picking up two remaining guest-visible paths, to avoid duplicate work:
libkernel_sys/Aio.cpp: the two timed waits (aio.changed.wait_for(lock, *wait)) for the AIO suspend/wait-complete calls. Besides the 15.6 ms steps, the timeout restarts on every wake inside the loop instead of being one deadline.libSceZlib/Export.cpp:sceZlibWaitForDone's timedcompletion.wait_for.Both would move to
TimedWait::Conditionwith a deadline fromTimedWait::DeadlineNanos, as Rudp, AvPlayer and EventFlag already do, one PR per library with a repeated short-timeout timing check. I'm leaving outSignals.cpp(#11167 coverssigtimedwait; the alarm wait in the same file can follow it) andlibSceUsbd(@hyvvs's, and #9339).AI-assisted: yes (Claude Code)
fix(libkernel_sys): wait for AIO timeouts until one deadline, precise…
fix(libSceZlib): end sceZlibWaitForDone timeouts on time on Windows