perf(host): on Linux the game's own short sleeps wait on the timer instead of yielding #2637

Merged
hckrman101 merged 1 commit from perf/linux-short-sleep-timer into main 2026-11-12 03:48:12 +00:00
hckrman101 commented 2026-11-12 03:40:17 +00:00 (Migrated from github.com)

Draft. On Linux, OoT's idle job workers burn a processor each spinning in a yield loop while one of them waits on the GPU. This makes the game's own sub-millisecond sleeps (SleepThread) wait on the timer instead, as Windows already does on 6-thread CPUs since #2263.

What it costs today

With #2602, Hyrule Field (RTX 3090, Ryzen 9 7900X, 24 threads, Linux, default options):

  • The five ModuleSystemWorker threads and sead::MainThread yield for 3.3-3.6 ms a frame each in sleep_away. That is about 9 ms of CPU a frame in all, while one of them waits on a GPU readback.
  • The cause: on Linux sleep_ns never counts the processors, so every sleep under a millisecond is a yield loop (host/hos_thread.c).

The change

On Linux, when HOS_SHORT_SLEEP_TIMER is not set, the game's own short sleeps wait on the timer (mode 1). The host's own short sleeps (the frame limiter, the audio renderer) keep yielding, so frame pacing is unchanged.

  • HOS_SHORT_SLEEP_TIMER=0: all yield, as before.
  • HOS_SHORT_SLEEP_TIMER=1: all on the timer.
  • Windows is unchanged.

Measured

Hyrule Field, one run each, all sleeps on the timer (HOS_SHORT_SLEEP_TIMER=1, mode 2, which also moves the host's sleeps). This PR's default (mode 1) has not been run yet.

median frame fps p99 CPU a frame, all threads
yield (main) 11.30 ms 84.2 17.4 ms 44.8 ms
timer for all 11.32 ms 81.8 16.3 ms 35.4 ms
  • The same median frame, and about 9 ms less CPU a frame (−21 %). An earlier run on main before #2602 also gave 37.5 ms against 64-73 ms.
  • On this 24-thread CPU the saving is CPU only. On 4-8 thread CPUs (the Steam Deck has 8) those yield loops take processors the frame needs; #2263 measured that on Windows (Hyrule Field 47 → 51 fps on 4 threads, and its long frames gone).
  • The cost: a job worker wakes about 0.5 ms later. In Hyrule Field the job workers are not on the frame's critical path (HOS_CHAIN). #2263 found TOTK's 20 µs waits on its critical path with the timer for all sleeps, which is why this changes only the game's own sleeps.

Tested

  • tests/test_short_sleep_timer.py passes. It covers hos_short_sleep_timer itself, which is unchanged; the new Linux default is in its caller.
  • A game build of this commit is running.

Not yet tested

  • This PR's mode (the game's sleeps only) in the game, as an A/B against main: fps, frames over 20/33 ms, CPU a frame. Hyrule Field and Kokiri, uncapped and at a 60 cap.
  • 4-8 thread CPUs and the Steam Deck, where it should matter most.
**Draft.** On Linux, OoT's idle job workers burn a processor each spinning in a yield loop while one of them waits on the GPU. This makes the game's own sub-millisecond sleeps (`SleepThread`) wait on the timer instead, as Windows already does on 6-thread CPUs since #2263. ## What it costs today With #2602, Hyrule Field (RTX 3090, Ryzen 9 7900X, 24 threads, Linux, default options): - The five `ModuleSystemWorker` threads and `sead::MainThread` yield for 3.3-3.6 ms a frame each in `sleep_away`. That is about 9 ms of CPU a frame in all, while one of them waits on a GPU readback. - The cause: on Linux `sleep_ns` never counts the processors, so every sleep under a millisecond is a yield loop (`host/hos_thread.c`). ## The change On Linux, when `HOS_SHORT_SLEEP_TIMER` is not set, the game's own short sleeps wait on the timer (mode 1). The host's own short sleeps (the frame limiter, the audio renderer) keep yielding, so frame pacing is unchanged. - `HOS_SHORT_SLEEP_TIMER=0`: all yield, as before. - `HOS_SHORT_SLEEP_TIMER=1`: all on the timer. - Windows is unchanged. ## Measured Hyrule Field, one run each, all sleeps on the timer (`HOS_SHORT_SLEEP_TIMER=1`, mode 2, which also moves the host's sleeps). This PR's default (mode 1) has not been run yet. | | median frame | fps | p99 | CPU a frame, all threads | |---|---|---|---|---| | yield (main) | 11.30 ms | 84.2 | 17.4 ms | 44.8 ms | | timer for all | 11.32 ms | 81.8 | 16.3 ms | **35.4 ms** | - **The same median frame, and about 9 ms less CPU a frame** (−21 %). An earlier run on main before #2602 also gave 37.5 ms against 64-73 ms. - On this 24-thread CPU the saving is CPU only. On 4-8 thread CPUs (the Steam Deck has 8) those yield loops take processors the frame needs; #2263 measured that on Windows (Hyrule Field 47 → 51 fps on 4 threads, and its long frames gone). - **The cost:** a job worker wakes about 0.5 ms later. In Hyrule Field the job workers are not on the frame's critical path (`HOS_CHAIN`). #2263 found TOTK's 20 µs waits on its critical path with the timer for all sleeps, which is why this changes only the game's own sleeps. ## Tested - `tests/test_short_sleep_timer.py` passes. It covers `hos_short_sleep_timer` itself, which is unchanged; the new Linux default is in its caller. - A game build of this commit is running. ## Not yet tested - **This PR's mode** (the game's sleeps only) in the game, as an A/B against main: fps, frames over 20/33 ms, CPU a frame. Hyrule Field and Kokiri, uncapped and at a 60 cap. - 4-8 thread CPUs and the Steam Deck, where it should matter most.
hckrman101 commented 2026-11-12 04:09:04 +00:00 (Migrated from github.com)

Measured after the merge, on main with this change: one alternating pair in Hyrule Field (uncapped, RTX on Linux/Wayland, 15 s measured after a 20 s settle), default against HOS_SHORT_SLEEP_TIMER=0 (the old behaviour).

Median frame Mean fps CPU a frame (all threads) p99 frame Longest frame
Default (timer) 11.66 ms 80.4 40.2 ms 17.0 ms 194 ms
HOS_SHORT_SLEEP_TIMER=0 11.57 ms 81.9 47.6 ms 17.5 ms 97 ms
  • Frame rate is the same within noise; CPU time is about 15 % lower (7.4 ms a frame).
  • The default side had one longer frame and a lower 1 % low (25 vs 36 fps) in this pair. That is a single pair, so it may be noise; if anyone sees more hitches since this merged, HOS_SHORT_SLEEP_TIMER=0 is the switch to compare with, and a report here would help.
Measured after the merge, on main with this change: one alternating pair in Hyrule Field (uncapped, RTX on Linux/Wayland, 15 s measured after a 20 s settle), default against `HOS_SHORT_SLEEP_TIMER=0` (the old behaviour). | | Median frame | Mean fps | CPU a frame (all threads) | p99 frame | Longest frame | |---|---|---|---|---|---| | Default (timer) | 11.66 ms | 80.4 | 40.2 ms | 17.0 ms | 194 ms | | `HOS_SHORT_SLEEP_TIMER=0` | 11.57 ms | 81.9 | 47.6 ms | 17.5 ms | 97 ms | - Frame rate is the same within noise; CPU time is about 15 % lower (7.4 ms a frame). - The default side had one longer frame and a lower 1 % low (25 vs 36 fps) in this pair. That is a single pair, so it may be noise; if anyone sees more hitches since this merged, `HOS_SHORT_SLEEP_TIMER=0` is the switch to compare with, and a report here would help.
Commenting is not possible because the repository is archived.
No description provided.