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}