#564·Weylus

Low FPS on Windows due to recv_timeout quantization

Author: xht0201Created Aug 21, 2026Updated Aug 21, 2026

Environment

  • OS: Windows 11
  • Weylus: 0.11.4
  • Client: iPad

Summary

On Windows the video frame loop runs at roughly half the intended rate (~23 fps instead of 60). Capture and encoding are NOT the bottleneck — the pacing sleep is. recv_timeout on the video command channel is quantized by the default 15.6 ms Windows timer tick, so the idle wait that should be ~6.5 ms pushing the frame period from 16.67 ms to ~27–31 ms.

Per-frame breakdown (measured on Windows 11)

metric before fix after `timeBeginPer
fps 23.1 52.2
period avg 27.1 ms 16.8 ms
wait avg ~16–27 ms 7.3 ms
capture avg 5.4 ms 4.9 ms
encode avg 4.8 ms 5.2 ms

Capture + encode ≈ 10 ms, well inside the 16.67 ms budget. The entire deficit is the wait.

Root cause

The video loop paces frames via receiver.recv_timeout(timeout) (std::sync::mpsc → WaitForSingleObject on Windows). Timed waits round up to the current system timer tick, which is 15.6 ms by default. Measured in isolation on the same machine:

requested timeout actual, before actual, after timeBeginPeriod(1)
6 ms 15.77 ms 6.50 ms
11 ms 15.60 ms 11.55 ms
16 ms 29.58 ms 16.58 ms

So a 6–11 ms pacing sleep lands on one tick (~15.6 ms) or two (~31 ms), roughly halving the frame rate.

Proposed fix

Raise the timer resolution once at startup and keep it for the process lifetime (the standard approach used by games/rendering apps):

rust
// main.rs, at the top of main(), Windows only
#[cfg(target_os = "windows")]
{
    winapi::um::timeapi::timeBeginPeriod(1);
}

Cargo.toml (winapi features):
winapi = { version = "...", features = [ ..., "mmsystem", "timeapi" ] }

Optionally pair with timeEndPeriod(1) on process exit.

Verification

After the change the wait drops to ~7 ms and the while capture/encode times are unchanged —confirming the deficit was purely the quantized sleep.