1use std::{
2 sync::LazyLock,
3 time::{Duration, Instant},
4};
5
6use anyhow::{Context, Result};
7use util::ResultExt;
8use windows::Win32::{
9 Foundation::HWND,
10 Graphics::Dwm::{DWM_TIMING_INFO, DwmFlush, DwmGetCompositionTimingInfo},
11 System::Performance::QueryPerformanceFrequency,
12};
13
14static QPC_TICKS_PER_SECOND: LazyLock<u64> = LazyLock::new(|| {
15 let mut frequency = 0;
16 // On systems that run Windows XP or later, the function will always succeed and
17 // will thus never return zero.
18 unsafe { QueryPerformanceFrequency(&mut frequency).unwrap() };
19 frequency as u64
20});
21
22const VSYNC_INTERVAL_THRESHOLD: Duration = Duration::from_millis(1);
23const DEFAULT_VSYNC_INTERVAL: Duration = Duration::from_micros(16_666); // ~60Hz
24
25pub(crate) struct VSyncProvider {
26 interval: Duration,
27 f: Box<dyn Fn() -> bool>,
28}
29
30impl VSyncProvider {
31 pub(crate) fn new() -> Self {
32 let interval = get_dwm_interval()
33 .context("Failed to get DWM interval")
34 .log_err()
35 .unwrap_or(DEFAULT_VSYNC_INTERVAL);
36 let f = Box::new(|| unsafe { DwmFlush().is_ok() });
37 Self { interval, f }
38 }
39
40 pub(crate) fn wait_for_vsync(&self) {
41 let vsync_start = Instant::now();
42 let wait_succeeded = (self.f)();
43 let elapsed = vsync_start.elapsed();
44 // DwmFlush and DCompositionWaitForCompositorClock returns very early
45 // instead of waiting until vblank when the monitor goes to sleep or is
46 // unplugged (nothing to present due to desktop occlusion). We use 1ms as
47 // a threshold for the duration of the wait functions and fallback to
48 // Sleep() if it returns before that. This could happen during normal
49 // operation for the first call after the vsync thread becomes non-idle,
50 // but it shouldn't happen often.
51 if !wait_succeeded || elapsed < VSYNC_INTERVAL_THRESHOLD {
52 log::trace!("VSyncProvider::wait_for_vsync() took less time than expected");
53 std::thread::sleep(self.interval);
54 }
55 }
56}
57
58fn get_dwm_interval() -> Result<Duration> {
59 let mut timing_info = DWM_TIMING_INFO {
60 cbSize: std::mem::size_of::<DWM_TIMING_INFO>() as u32,
61 ..Default::default()
62 };
63 unsafe { DwmGetCompositionTimingInfo(HWND::default(), &mut timing_info) }?;
64 let interval = retrieve_duration(timing_info.qpcRefreshPeriod, *QPC_TICKS_PER_SECOND);
65 // Check for interval values that are impossibly low. A 29 microsecond
66 // interval was seen (from a qpcRefreshPeriod of 60).
67 if interval < VSYNC_INTERVAL_THRESHOLD {
68 Ok(retrieve_duration(
69 timing_info.rateRefresh.uiDenominator as u64,
70 timing_info.rateRefresh.uiNumerator as u64,
71 ))
72 } else {
73 Ok(interval)
74 }
75}
76
77#[inline]
78fn retrieve_duration(counts: u64, ticks_per_second: u64) -> Duration {
79 let ticks_per_microsecond = ticks_per_second / 1_000_000;
80 Duration::from_micros(counts / ticks_per_microsecond)
81}