use std::{
sync::LazyLock,
time::{Duration, Instant},
};
use anyhow::{Context, Result};
use util::ResultExt;
use windows::{
Win32::{
Foundation::{HANDLE, HWND},
Graphics::{
DirectComposition::{
COMPOSITION_FRAME_ID_COMPLETED, COMPOSITION_FRAME_ID_TYPE, COMPOSITION_FRAME_STATS,
COMPOSITION_TARGET_ID,
},
Dwm::{DWM_TIMING_INFO, DwmFlush, DwmGetCompositionTimingInfo},
},
System::{
LibraryLoader::{GetModuleHandleA, GetProcAddress},
Performance::QueryPerformanceFrequency,
Threading::INFINITE,
},
},
core::{HRESULT, s},
};
static QPC_TICKS_PER_SECOND: LazyLock<u64> = LazyLock::new(|| {
let mut frequency = 0;
unsafe { QueryPerformanceFrequency(&mut frequency).unwrap() };
frequency as u64
});
const VSYNC_INTERVAL_THRESHOLD: Duration = Duration::from_millis(1);
const DEFAULT_VSYNC_INTERVAL: Duration = Duration::from_micros(16_666);
type DCompositionGetFrameId =
unsafe extern "system" fn(frameidtype: COMPOSITION_FRAME_ID_TYPE, frameid: *mut u64) -> HRESULT;
type DCompositionGetStatistics = unsafe extern "system" fn(
frameid: u64,
framestats: *mut COMPOSITION_FRAME_STATS,
targetidcount: u32,
targetids: *mut COMPOSITION_TARGET_ID,
actualtargetidcount: *mut u32,
) -> HRESULT;
type DCompositionWaitForCompositorClock =
unsafe extern "system" fn(count: u32, handles: *const HANDLE, timeoutinms: u32) -> u32;
pub(crate) struct VSyncProvider {
interval: Duration,
f: Box<dyn Fn() -> bool>,
}
impl VSyncProvider {
pub(crate) fn new() -> Self {
if let Some((get_frame_id, get_statistics, wait_for_comp_clock)) =
initialize_direct_composition()
.context("Retrieving DirectComposition functions")
.log_with_level(log::Level::Warn)
{
let interval = get_dwm_interval_from_direct_composition(get_frame_id, get_statistics)
.context("Failed to get DWM interval from DirectComposition")
.log_err()
.unwrap_or(DEFAULT_VSYNC_INTERVAL);
log::info!(
"DirectComposition is supported for VSync, interval: {:?}",
interval
);
let f = Box::new(move || unsafe {
wait_for_comp_clock(0, std::ptr::null(), INFINITE) == 0
});
Self { interval, f }
} else {
let interval = get_dwm_interval()
.context("Failed to get DWM interval")
.log_err()
.unwrap_or(DEFAULT_VSYNC_INTERVAL);
log::info!(
"DirectComposition is not supported for VSync, falling back to DWM, interval: {:?}",
interval
);
let f = Box::new(|| unsafe { DwmFlush().is_ok() });
Self { interval, f }
}
}
pub(crate) fn wait_for_vsync(&self) {
let vsync_start = Instant::now();
let wait_succeeded = (self.f)();
let elapsed = vsync_start.elapsed();
if !wait_succeeded || elapsed < VSYNC_INTERVAL_THRESHOLD {
log::trace!("VSyncProvider::wait_for_vsync() took less time than expected");
std::thread::sleep(self.interval);
}
}
}
fn initialize_direct_composition() -> Result<(
DCompositionGetFrameId,
DCompositionGetStatistics,
DCompositionWaitForCompositorClock,
)> {
unsafe {
let hmodule = GetModuleHandleA(s!("dcomp.dll")).context("Loading dcomp.dll")?;
let get_frame_id_addr = GetProcAddress(hmodule, s!("DCompositionGetFrameId"))
.context("Function DCompositionGetFrameId not found")?;
let get_statistics_addr = GetProcAddress(hmodule, s!("DCompositionGetStatistics"))
.context("Function DCompositionGetStatistics not found")?;
let wait_for_compositor_clock_addr =
GetProcAddress(hmodule, s!("DCompositionWaitForCompositorClock"))
.context("Function DCompositionWaitForCompositorClock not found")?;
let get_frame_id: DCompositionGetFrameId = std::mem::transmute(get_frame_id_addr);
let get_statistics: DCompositionGetStatistics = std::mem::transmute(get_statistics_addr);
let wait_for_compositor_clock: DCompositionWaitForCompositorClock =
std::mem::transmute(wait_for_compositor_clock_addr);
Ok((get_frame_id, get_statistics, wait_for_compositor_clock))
}
}
fn get_dwm_interval_from_direct_composition(
get_frame_id: DCompositionGetFrameId,
get_statistics: DCompositionGetStatistics,
) -> Result<Duration> {
let mut frame_id = 0;
unsafe { get_frame_id(COMPOSITION_FRAME_ID_COMPLETED, &mut frame_id) }.ok()?;
let mut stats = COMPOSITION_FRAME_STATS::default();
unsafe {
get_statistics(
frame_id,
&mut stats,
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
)
}
.ok()?;
Ok(retrieve_duration(stats.framePeriod, *QPC_TICKS_PER_SECOND))
}
fn get_dwm_interval() -> Result<Duration> {
let mut timing_info = DWM_TIMING_INFO {
cbSize: std::mem::size_of::<DWM_TIMING_INFO>() as u32,
..Default::default()
};
unsafe { DwmGetCompositionTimingInfo(HWND::default(), &mut timing_info) }?;
let interval = retrieve_duration(timing_info.qpcRefreshPeriod, *QPC_TICKS_PER_SECOND);
if interval < VSYNC_INTERVAL_THRESHOLD {
Ok(retrieve_duration(
timing_info.rateRefresh.uiDenominator as u64,
timing_info.rateRefresh.uiNumerator as u64,
))
} else {
Ok(interval)
}
}
#[inline]
fn retrieve_duration(counts: u64, ticks_per_second: u64) -> Duration {
let ticks_per_microsecond = ticks_per_second / 1_000_000;
Duration::from_micros(counts / ticks_per_microsecond)
}