Skip to content

fix(linux): sleep short waits on the timer instead of yielding - #2313

Closed
Kolkka wants to merge 2 commits into
NotRexed:mainfrom
Kolkka:linux/short-sleep-timer
Closed

Kolkka wants to merge 2 commits into
NotRexed:mainfrom
Kolkka:linux/short-sleep-timer

Conversation

@Kolkka

@Kolkka Kolkka commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

On Linux, the game's many short waits spin the CPU in the kernel. On the Steam Deck that takes the power the GPU needs: Kokiri Forest runs at 6.6 fps.

What changed: sleeps under 1 ms yield in a loop until the time, because Windows' timer is too coarse (~0.5 ms) for them. On Linux, clock_nanosleep wakes within ~50 µs, so the loop only burned CPU. OoT's job wait (f_800998a0, a 20 µs SleepThread between checks) ran about 35,000 times a second, and each worker spent 0.4–0.6 s per second in the kernel with ~20,000 context switches. The Deck's APU shares 15 W, so the CPU took about 11.8 W and the GPU was held at ~220 MHz. Linux now sleeps short waits on the timer, and Windows keeps the yield loop. HOS_SHORT_SLEEP_TIMER=0 restores the yields on Linux.

Tested: Steam Deck LCD, Kokiri Forest, standing still, 50% resolution:

Kernel time per job worker Kokiri fps
Before 0.4–0.6 s/s 6.4–6.6
After ~0.04 s/s 7.9–8.3

The rest of the gap is the Deck's power split: with the GPU clock set manually, the same build runs at 14.3 fps.

tests/run_all.py: no new failures against main.

🤖 Generated with Claude Code

On Linux, sleeps under a millisecond spun on sched_yield. OoT's job wait (a
20 us SleepThread) ran 35,000 times a second on the Steam Deck, each worker
0.4-0.6 s a second in the kernel, and the APU's 15 W went to the CPU with the
GPU held at ~220 MHz (Kokiri 6.6 fps; 7.9 with the timer). Linux's
clock_nanosleep wakes within ~50 us, so short sleeps use it there; Windows
keeps the yield loop. HOS_SHORT_SLEEP_TIMER=0 restores the yields on Linux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017tiHucJGVjM9vhcGkzmPMu

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The measurements make a good case: on Linux the yield loop only burns CPU, and on the Deck that power comes out of the GPU's share. I'd like this fix in. But main changed this exact block an hour before the PR was opened, so it can't merge as it is.

blocking: merge conflict in host/hos_thread.c with #2263 (4d488bc). That PR moved the decision into hos_short_sleep_timer(asked, threads) and gave sleep_ns a guest argument. Short sleeps can now go three ways: all yield; the game's own sleeps (SleepThread, through hos_sleep_logged) wait on the timer while the host's yield; or all wait on the timer. git merge-tree against origin/main conflicts in this hunk. Rather than resolving the conflict line by line, I'd fold the Linux rule into that design. For example, on non-Windows, non-web builds, have the unset case return 1 (or 2, see below) instead of going through the processor count, and keep hos_short_sleep_timer the single place the choice is made.

blocking: host/hos_thread.c:874 reads the switch with atoi(getenv(...)) != 0. Since #2042, switches follow env_on/env_value_off, and HOS_SHORT_SLEEP_TIMER=on (or true/yes) would parse as 0 here and turn the timer off, the opposite of what was asked. Main's tests/test_short_sleep_timer.py has a broken variant for exactly this ("read as a number instead of by env_on's rule"). Reusing hos_short_sleep_timer fixes it for free.

should fix: decide on purpose which sleeps the timer applies to. As written, the PR puts every sleep under 1 ms on the Linux timer, including the host's: the frame limiter (host/nn_vi.c:30, :177) and the audio renderer (host/nn_audio.c:1168). #2263 deliberately kept those yielding so that pacing isn't late. With Linux's ~50 µs slack that may well be fine, but it is a separate change from the job-worker fix you measured. Either use mode 1 (only the game's sleeps on the timer), which covers f_800998a0, or keep mode 2 and say in the PR that frame pacing and audio were checked on the Deck.

should fix: tests. tests/test_short_sleep_timer.py forces _WIN32 on the cut code (line 53), so it would not see the Linux default at all. Please add cases for the non-Windows build: unset means the timer, and 0/off means yield. Also run that test against the rebased head. Right now CI has only run the title check; the other workflows are waiting for approval.

minor: the log line in main's version says "where Windows has the high-resolution one". If Linux now logs through the same path, word it for both platforms. The measurement covered the Deck only. A desktop Linux run with many threads (where #2263 found yielding helps TOTK-style 20 µs waits on Windows) would show whether "always the timer on Linux" or "the timer by processor count" is the better rule. The ~50 µs slack makes me expect the former, but it's worth one number.

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It conflicts in host/hos_thread.c with #2263's hos_short_sleep_timer, so it needs a rebase onto that. Two other things:

  • atoi(getenv("HOS_SHORT_SLEEP_TIMER")) != 0 makes =on turn the timer off, which tests/test_short_sleep_timer.py exists to catch.
  • As written, the change also moves the frame limiter (nn_vi.c) and audio (nn_audio.c) sleeps onto the timer, not just the job waits that were measured.

@hckrman101

ghost commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

This now conflicts with main in host/hos_thread.c. Could you merge main into the branch (or rebase) when you get a chance? I'll take another look once it's up to date.

Resolves host/hos_thread.c against main's processor-count choice
(hos_short_sleep_timer): Windows keeps main's logic unchanged; elsewhere
short sleeps use the timer by default, and HOS_SHORT_SLEEP_TIMER=0 restores
the yields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017tiHuZvvYyHV6LgKegyNrk
@Kolkka

Kolkka commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

Merged main into the branch.

@Kolkka

Kolkka commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #2637

@Kolkka Kolkka closed this Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants