vsync.rs

  1use std::{
  2    sync::LazyLock,
  3    time::{Duration, Instant},
  4};
  5
  6use anyhow::{Context, Result};
  7use util::ResultExt;
  8use windows::{
  9    Win32::{
 10        Foundation::{HANDLE, HWND},
 11        Graphics::{
 12            DirectComposition::{
 13                COMPOSITION_FRAME_ID_COMPLETED, COMPOSITION_FRAME_ID_TYPE, COMPOSITION_FRAME_STATS,
 14                COMPOSITION_TARGET_ID,
 15            },
 16            Dwm::{DWM_TIMING_INFO, DwmFlush, DwmGetCompositionTimingInfo},
 17        },
 18        System::{
 19            LibraryLoader::{GetModuleHandleA, GetProcAddress},
 20            Performance::QueryPerformanceFrequency,
 21            Threading::INFINITE,
 22        },
 23    },
 24    core::{HRESULT, s},
 25};
 26
 27static QPC_TICKS_PER_SECOND: LazyLock<u64> = LazyLock::new(|| {
 28    let mut frequency = 0;
 29    // On systems that run Windows XP or later, the function will always succeed and
 30    // will thus never return zero.
 31    unsafe { QueryPerformanceFrequency(&mut frequency).unwrap() };
 32    frequency as u64
 33});
 34
 35const VSYNC_INTERVAL_THRESHOLD: Duration = Duration::from_millis(1);
 36const DEFAULT_VSYNC_INTERVAL: Duration = Duration::from_micros(16_666); // ~60Hz
 37
 38// Here we are using dynamic loading of DirectComposition functions,
 39// or the app will refuse to start on windows systems that do not support DirectComposition.
 40type DCompositionGetFrameId =
 41    unsafe extern "system" fn(frameidtype: COMPOSITION_FRAME_ID_TYPE, frameid: *mut u64) -> HRESULT;
 42type DCompositionGetStatistics = unsafe extern "system" fn(
 43    frameid: u64,
 44    framestats: *mut COMPOSITION_FRAME_STATS,
 45    targetidcount: u32,
 46    targetids: *mut COMPOSITION_TARGET_ID,
 47    actualtargetidcount: *mut u32,
 48) -> HRESULT;
 49type DCompositionWaitForCompositorClock =
 50    unsafe extern "system" fn(count: u32, handles: *const HANDLE, timeoutinms: u32) -> u32;
 51
 52pub(crate) struct VSyncProvider {
 53    interval: Duration,
 54    f: Box<dyn Fn() -> bool>,
 55}
 56
 57impl VSyncProvider {
 58    pub(crate) fn new() -> Self {
 59        if let Some((get_frame_id, get_statistics, wait_for_comp_clock)) =
 60            initialize_direct_composition()
 61                .context("Retrieving DirectComposition functions")
 62                .log_with_level(log::Level::Warn)
 63        {
 64            let interval = get_dwm_interval_from_direct_composition(get_frame_id, get_statistics)
 65                .context("Failed to get DWM interval from DirectComposition")
 66                .log_err()
 67                .unwrap_or(DEFAULT_VSYNC_INTERVAL);
 68            log::info!(
 69                "DirectComposition is supported for VSync, interval: {:?}",
 70                interval
 71            );
 72            let f = Box::new(move || unsafe {
 73                wait_for_comp_clock(0, std::ptr::null(), INFINITE) == 0
 74            });
 75            Self { interval, f }
 76        } else {
 77            let interval = get_dwm_interval()
 78                .context("Failed to get DWM interval")
 79                .log_err()
 80                .unwrap_or(DEFAULT_VSYNC_INTERVAL);
 81            log::info!(
 82                "DirectComposition is not supported for VSync, falling back to DWM, interval: {:?}",
 83                interval
 84            );
 85            let f = Box::new(|| unsafe { DwmFlush().is_ok() });
 86            Self { interval, f }
 87        }
 88    }
 89
 90    pub(crate) fn wait_for_vsync(&self) {
 91        let vsync_start = Instant::now();
 92        let wait_succeeded = (self.f)();
 93        let elapsed = vsync_start.elapsed();
 94        // DwmFlush and DCompositionWaitForCompositorClock returns very early
 95        // instead of waiting until vblank when the monitor goes to sleep or is
 96        // unplugged (nothing to present due to desktop occlusion). We use 1ms as
 97        // a threshold for the duration of the wait functions and fallback to
 98        // Sleep() if it returns before that. This could happen during normal
 99        // operation for the first call after the vsync thread becomes non-idle,
100        // but it shouldn't happen often.
101        if !wait_succeeded || elapsed < VSYNC_INTERVAL_THRESHOLD {
102            log::trace!("VSyncProvider::wait_for_vsync() took less time than expected");
103            std::thread::sleep(self.interval);
104        }
105    }
106}
107
108fn initialize_direct_composition() -> Result<(
109    DCompositionGetFrameId,
110    DCompositionGetStatistics,
111    DCompositionWaitForCompositorClock,
112)> {
113    unsafe {
114        // Load DLL at runtime since older Windows versions don't have dcomp.
115        let hmodule = GetModuleHandleA(s!("dcomp.dll")).context("Loading dcomp.dll")?;
116        let get_frame_id_addr = GetProcAddress(hmodule, s!("DCompositionGetFrameId"))
117            .context("Function DCompositionGetFrameId not found")?;
118        let get_statistics_addr = GetProcAddress(hmodule, s!("DCompositionGetStatistics"))
119            .context("Function DCompositionGetStatistics not found")?;
120        let wait_for_compositor_clock_addr =
121            GetProcAddress(hmodule, s!("DCompositionWaitForCompositorClock"))
122                .context("Function DCompositionWaitForCompositorClock not found")?;
123        let get_frame_id: DCompositionGetFrameId = std::mem::transmute(get_frame_id_addr);
124        let get_statistics: DCompositionGetStatistics = std::mem::transmute(get_statistics_addr);
125        let wait_for_compositor_clock: DCompositionWaitForCompositorClock =
126            std::mem::transmute(wait_for_compositor_clock_addr);
127        Ok((get_frame_id, get_statistics, wait_for_compositor_clock))
128    }
129}
130
131fn get_dwm_interval_from_direct_composition(
132    get_frame_id: DCompositionGetFrameId,
133    get_statistics: DCompositionGetStatistics,
134) -> Result<Duration> {
135    let mut frame_id = 0;
136    unsafe { get_frame_id(COMPOSITION_FRAME_ID_COMPLETED, &mut frame_id) }.ok()?;
137    let mut stats = COMPOSITION_FRAME_STATS::default();
138    unsafe {
139        get_statistics(
140            frame_id,
141            &mut stats,
142            0,
143            std::ptr::null_mut(),
144            std::ptr::null_mut(),
145        )
146    }
147    .ok()?;
148    Ok(retrieve_duration(stats.framePeriod, *QPC_TICKS_PER_SECOND))
149}
150
151fn get_dwm_interval() -> Result<Duration> {
152    let mut timing_info = DWM_TIMING_INFO {
153        cbSize: std::mem::size_of::<DWM_TIMING_INFO>() as u32,
154        ..Default::default()
155    };
156    unsafe { DwmGetCompositionTimingInfo(HWND::default(), &mut timing_info) }?;
157    let interval = retrieve_duration(timing_info.qpcRefreshPeriod, *QPC_TICKS_PER_SECOND);
158    // Check for interval values that are impossibly low. A 29 microsecond
159    // interval was seen (from a qpcRefreshPeriod of 60).
160    if interval < VSYNC_INTERVAL_THRESHOLD {
161        Ok(retrieve_duration(
162            timing_info.rateRefresh.uiDenominator as u64,
163            timing_info.rateRefresh.uiNumerator as u64,
164        ))
165    } else {
166        Ok(interval)
167    }
168}
169
170#[inline]
171fn retrieve_duration(counts: u64, ticks_per_second: u64) -> Duration {
172    let ticks_per_microsecond = ticks_per_second / 1_000_000;
173    Duration::from_micros(counts / ticks_per_microsecond)
174}