Skip to main content

pfx_gpu/
window.rs

1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5const NS_PER_SEC: u64 = 1_000_000_000;
6const FIFO_LATENCY: u32 = 2;
7const VRR_LATENCY: u32 = 1;
8const VRR_HEADROOM_MHZ: u32 = 3_000;
9
10const GAME_MODES: [wgpu::PresentMode; 3] = [
11    wgpu::PresentMode::Mailbox,
12    wgpu::PresentMode::FifoRelaxed,
13    wgpu::PresentMode::Fifo,
14];
15const MACOS_VRR: [wgpu::PresentMode; 2] = [wgpu::PresentMode::Mailbox, wgpu::PresentMode::Fifo];
16const X11_VRR: [wgpu::PresentMode; 3] = [
17    wgpu::PresentMode::Mailbox,
18    wgpu::PresentMode::Fifo,
19    wgpu::PresentMode::FifoRelaxed,
20];
21const WAYLAND_VRR: [wgpu::PresentMode; 2] = [wgpu::PresentMode::Mailbox, wgpu::PresentMode::Fifo];
22const FIFO_ONLY: [wgpu::PresentMode; 1] = [wgpu::PresentMode::Fifo];
23
24pub const LAYOUT_UNITS: Size = Size {
25    width: 1920,
26    height: 1080,
27};
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
30pub struct Size {
31    pub width: u32,
32    pub height: u32,
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
36pub struct Point {
37    pub x: i32,
38    pub y: i32,
39}
40
41#[derive(Clone, Copy, Debug, PartialEq)]
42pub struct Rect {
43    pub x: f32,
44    pub y: f32,
45    pub width: f32,
46    pub height: f32,
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub struct VideoMode {
51    pub size: Size,
52    pub bit_depth: u16,
53    pub refresh_millihertz: u32,
54}
55
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct Monitor {
58    pub name: Option<String>,
59    pub position: Point,
60    pub size: Size,
61    pub refresh_millihertz: Option<u32>,
62    pub video_modes: Vec<VideoMode>,
63}
64
65#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct Monitors {
67    pub list: Vec<Monitor>,
68    pub primary: Option<usize>,
69    pub current: Option<usize>,
70}
71
72#[derive(Clone, Debug, PartialEq, Eq)]
73pub enum MonitorChoice {
74    Current,
75    Primary,
76    Index(usize),
77    Name(String),
78}
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub enum VideoModeChoice {
82    Exact(VideoMode),
83    SizeRefresh { size: Size, refresh_millihertz: u32 },
84    Native,
85}
86
87#[derive(Clone, Debug, PartialEq, Eq)]
88pub enum DisplayMode {
89    Windowed {
90        size: Size,
91        position: Option<Point>,
92    },
93    BorderlessWindowed {
94        size: Size,
95        position: Option<Point>,
96    },
97    BorderlessFullscreen {
98        monitor: MonitorChoice,
99    },
100    ExclusiveFullscreen {
101        monitor: MonitorChoice,
102        video_mode: VideoModeChoice,
103    },
104}
105
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107pub struct PlatformCaps {
108    pub exclusive: bool,
109}
110
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112pub enum Host {
113    Windows,
114    Macos,
115    X11,
116    Wayland,
117    Other,
118}
119
120impl Host {
121    pub fn supports_exclusive(self) -> bool {
122        matches!(self, Host::Windows | Host::Macos | Host::X11)
123    }
124
125    pub fn vrr_present_modes(self) -> &'static [wgpu::PresentMode] {
126        match self {
127            Host::Windows => &GAME_MODES,
128            Host::Macos => &MACOS_VRR,
129            Host::X11 => &X11_VRR,
130            Host::Wayland => &WAYLAND_VRR,
131            Host::Other => &FIFO_ONLY,
132        }
133    }
134}
135
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub enum Fallback {
138    ExclusiveUnsupported,
139    VideoModeMissing,
140    MonitorMissing,
141}
142
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub enum FullscreenPlan {
145    Windowed,
146    Borderless { monitor: usize },
147    Exclusive { monitor: usize, mode: VideoMode },
148}
149
150#[derive(Clone, Copy, Debug, PartialEq, Eq)]
151pub struct ModePlan {
152    pub fullscreen: FullscreenPlan,
153    pub decorations: bool,
154    pub resizable: bool,
155    pub inner_size: Option<Size>,
156    pub outer_position: Option<Point>,
157    pub fallback: Option<Fallback>,
158    pub monitor: Option<usize>,
159}
160
161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
162pub enum PlanError {
163    EmptySize,
164    NoMonitor,
165}
166
167impl fmt::Display for PlanError {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        match self {
170            PlanError::EmptySize => write!(f, "window size must be nonzero"),
171            PlanError::NoMonitor => write!(f, "no monitor to fullscreen on"),
172        }
173    }
174}
175
176impl std::error::Error for PlanError {}
177
178pub trait WindowOps {
179    fn set_fullscreen(&mut self, fullscreen: &FullscreenPlan);
180    fn set_decorations(&mut self, decorations: bool);
181    fn request_inner_size(&mut self, size: Size);
182    fn set_outer_position(&mut self, position: Point);
183    fn set_resizable(&mut self, _resizable: bool) {}
184}
185
186pub fn plan_display_mode(
187    mode: &DisplayMode,
188    platform: PlatformCaps,
189    monitors: &Monitors,
190) -> Result<ModePlan, PlanError> {
191    match mode {
192        DisplayMode::Windowed { size, position } => placed(*size, *position, true, monitors),
193        DisplayMode::BorderlessWindowed { size, position } => {
194            placed(*size, *position, false, monitors)
195        }
196        DisplayMode::BorderlessFullscreen { monitor } => borderless(monitor, monitors, None),
197        DisplayMode::ExclusiveFullscreen {
198            monitor,
199            video_mode,
200        } => exclusive(monitor, video_mode, platform, monitors),
201    }
202}
203
204pub fn apply_plan(plan: &ModePlan, window: &mut impl WindowOps) {
205    match plan.fullscreen {
206        FullscreenPlan::Windowed => {
207            window.set_fullscreen(&plan.fullscreen);
208            window.set_decorations(plan.decorations);
209            window.set_resizable(plan.resizable);
210            if let Some(position) = plan.outer_position {
211                window.set_outer_position(position);
212            }
213            if let Some(size) = plan.inner_size {
214                window.request_inner_size(size);
215            }
216        }
217        FullscreenPlan::Borderless { .. } | FullscreenPlan::Exclusive { .. } => {
218            window.set_decorations(plan.decorations);
219            window.set_fullscreen(&plan.fullscreen);
220        }
221    }
222}
223
224fn placed(
225    size: Size,
226    position: Option<Point>,
227    decorations: bool,
228    monitors: &Monitors,
229) -> Result<ModePlan, PlanError> {
230    if size.width == 0 || size.height == 0 {
231        return Err(PlanError::EmptySize);
232    }
233    Ok(ModePlan {
234        fullscreen: FullscreenPlan::Windowed,
235        decorations,
236        resizable: decorations,
237        inner_size: Some(size),
238        outer_position: position,
239        fallback: None,
240        monitor: monitors.current,
241    })
242}
243
244fn resolve(
245    choice: &MonitorChoice,
246    monitors: &Monitors,
247) -> Result<(usize, Option<Fallback>), PlanError> {
248    if monitors.list.is_empty() {
249        return Err(PlanError::NoMonitor);
250    }
251    let found = match choice {
252        MonitorChoice::Current => monitors.current,
253        MonitorChoice::Primary => monitors.primary,
254        MonitorChoice::Index(index) => (*index < monitors.list.len()).then_some(*index),
255        MonitorChoice::Name(name) => monitors
256            .list
257            .iter()
258            .position(|monitor| monitor.name.as_deref() == Some(name.as_str())),
259    };
260    if let Some(index) = found {
261        return Ok((index, None));
262    }
263    let index = monitors.current.or(monitors.primary).unwrap_or(0);
264    Ok((index, Some(Fallback::MonitorMissing)))
265}
266
267fn borderless_plan(monitor: usize, fallback: Option<Fallback>) -> ModePlan {
268    ModePlan {
269        fullscreen: FullscreenPlan::Borderless { monitor },
270        decorations: false,
271        resizable: false,
272        inner_size: None,
273        outer_position: None,
274        fallback,
275        monitor: Some(monitor),
276    }
277}
278
279fn borderless(
280    choice: &MonitorChoice,
281    monitors: &Monitors,
282    fallback: Option<Fallback>,
283) -> Result<ModePlan, PlanError> {
284    let (monitor, missed) = resolve(choice, monitors)?;
285    Ok(borderless_plan(monitor, fallback.or(missed)))
286}
287
288fn exclusive(
289    choice: &MonitorChoice,
290    video_mode: &VideoModeChoice,
291    platform: PlatformCaps,
292    monitors: &Monitors,
293) -> Result<ModePlan, PlanError> {
294    let (monitor, missed) = resolve(choice, monitors)?;
295    if !platform.exclusive {
296        return Ok(borderless_plan(
297            monitor,
298            missed.or(Some(Fallback::ExclusiveUnsupported)),
299        ));
300    }
301    if let Some(mode) = find_video_mode(&monitors.list[monitor], video_mode) {
302        return Ok(ModePlan {
303            fullscreen: FullscreenPlan::Exclusive { monitor, mode },
304            decorations: false,
305            resizable: false,
306            inner_size: None,
307            outer_position: None,
308            fallback: missed,
309            monitor: Some(monitor),
310        });
311    }
312    Ok(borderless_plan(
313        monitor,
314        missed.or(Some(Fallback::VideoModeMissing)),
315    ))
316}
317
318fn find_video_mode(monitor: &Monitor, choice: &VideoModeChoice) -> Option<VideoMode> {
319    match choice {
320        VideoModeChoice::Exact(wanted) => monitor
321            .video_modes
322            .iter()
323            .copied()
324            .find(|mode| *mode == *wanted),
325        VideoModeChoice::SizeRefresh {
326            size,
327            refresh_millihertz,
328        } => monitor
329            .video_modes
330            .iter()
331            .copied()
332            .filter(|mode| mode.size == *size && mode.refresh_millihertz == *refresh_millihertz)
333            .max_by_key(|mode| mode.bit_depth),
334        VideoModeChoice::Native => monitor
335            .video_modes
336            .iter()
337            .copied()
338            .filter(|mode| mode.size == monitor.size)
339            .max_by_key(|mode| {
340                let current = monitor
341                    .refresh_millihertz
342                    .map(|refresh| mode.refresh_millihertz.abs_diff(refresh) <= 1_000);
343                (current, mode.refresh_millihertz, mode.bit_depth)
344            }),
345    }
346}
347
348#[derive(Clone, Debug, PartialEq, Eq)]
349pub struct PresentPreference {
350    pub modes: Vec<wgpu::PresentMode>,
351    pub desired_maximum_frame_latency: u32,
352}
353
354impl Default for PresentPreference {
355    fn default() -> Self {
356        Self::fifo()
357    }
358}
359
360impl PresentPreference {
361    pub fn fifo() -> Self {
362        Self {
363            modes: vec![wgpu::PresentMode::Fifo],
364            desired_maximum_frame_latency: FIFO_LATENCY,
365        }
366    }
367
368    pub fn game() -> Self {
369        Self {
370            modes: GAME_MODES.to_vec(),
371            desired_maximum_frame_latency: FIFO_LATENCY,
372        }
373    }
374
375    pub fn vrr(host: Host) -> Self {
376        Self {
377            modes: host.vrr_present_modes().to_vec(),
378            desired_maximum_frame_latency: VRR_LATENCY,
379        }
380    }
381}
382
383#[derive(Clone, Debug, PartialEq, Eq)]
384pub struct PresentChoice {
385    pub requested: Vec<wgpu::PresentMode>,
386    pub selected: wgpu::PresentMode,
387    pub fell_back: bool,
388    pub desired_maximum_frame_latency: u32,
389}
390
391pub fn choose_present(
392    preference: &PresentPreference,
393    available: &[wgpu::PresentMode],
394) -> PresentChoice {
395    let requested = if preference.modes.is_empty() {
396        vec![wgpu::PresentMode::Fifo]
397    } else {
398        preference.modes.clone()
399    };
400    let matched = requested
401        .iter()
402        .copied()
403        .find(|mode| available.contains(mode));
404    let (selected, fell_back) = match matched {
405        Some(mode) => (mode, false),
406        None => (wgpu::PresentMode::Fifo, true),
407    };
408    PresentChoice {
409        requested,
410        selected,
411        fell_back,
412        desired_maximum_frame_latency: preference.desired_maximum_frame_latency.max(1),
413    }
414}
415
416#[derive(Clone, Copy, Debug, PartialEq)]
417pub enum RenderScale {
418    Native,
419    Factor(f32),
420    Size(Size),
421}
422
423pub fn render_size(window: Size, scale: RenderScale) -> Option<Size> {
424    match scale {
425        RenderScale::Native => nonzero(window),
426        RenderScale::Size(size) => nonzero(size),
427        RenderScale::Factor(factor) => scaled(window, factor),
428    }
429}
430
431fn nonzero(size: Size) -> Option<Size> {
432    (size.width > 0 && size.height > 0).then_some(size)
433}
434
435fn scaled(window: Size, factor: f32) -> Option<Size> {
436    if !factor.is_finite() || factor <= 0.0 || window.width == 0 || window.height == 0 {
437        return None;
438    }
439    let width = (f64::from(window.width) * f64::from(factor)).round();
440    let height = (f64::from(window.height) * f64::from(factor)).round();
441    if !(1.0..f64::from(u32::MAX)).contains(&width) || !(1.0..f64::from(u32::MAX)).contains(&height)
442    {
443        return None;
444    }
445    Some(Size {
446        width: width as u32,
447        height: height as u32,
448    })
449}
450
451#[derive(Clone, Copy, Debug, PartialEq)]
452pub struct Letterbox {
453    pub matrix: [f32; 16],
454    pub inverse: [f32; 16],
455    pub visible: Rect,
456}
457
458impl Letterbox {
459    pub fn map(&self, layout_x: f32, layout_y: f32) -> [f32; 2] {
460        transform(&self.matrix, layout_x, layout_y)
461    }
462
463    pub fn unmap(&self, target_x: f32, target_y: f32) -> [f32; 2] {
464        transform(&self.inverse, target_x, target_y)
465    }
466
467    pub fn contains(&self, target_x: f32, target_y: f32) -> bool {
468        let visible = self.visible;
469        target_x >= visible.x
470            && target_y >= visible.y
471            && target_x < visible.x + visible.width
472            && target_y < visible.y + visible.height
473    }
474}
475
476pub fn letterbox(layout: Size, target: Size) -> Option<Letterbox> {
477    if layout.width == 0 || layout.height == 0 || target.width == 0 || target.height == 0 {
478        return None;
479    }
480    let layout_w = u64::from(layout.width);
481    let layout_h = u64::from(layout.height);
482    let target_w = u64::from(target.width);
483    let target_h = u64::from(target.height);
484    let (visible_w, visible_h, x, y) = if target_w * layout_h >= layout_w * target_h {
485        let visible_w = u32::try_from(target_h * layout_w / layout_h).ok()?;
486        let gap = target.width.checked_sub(visible_w)?;
487        (visible_w, target.height, gap / 2, 0)
488    } else {
489        let visible_h = u32::try_from(target_w * layout_h / layout_w).ok()?;
490        let gap = target.height.checked_sub(visible_h)?;
491        (target.width, visible_h, 0, gap / 2)
492    };
493    if visible_w == 0 || visible_h == 0 {
494        return None;
495    }
496    let sx = visible_w as f32 / layout.width as f32;
497    let sy = visible_h as f32 / layout.height as f32;
498    let tx = x as f32;
499    let ty = y as f32;
500    Some(Letterbox {
501        matrix: affine(sx, sy, tx, ty),
502        inverse: affine(1.0 / sx, 1.0 / sy, -tx / sx, -ty / sy),
503        visible: Rect {
504            x: tx,
505            y: ty,
506            width: visible_w as f32,
507            height: visible_h as f32,
508        },
509    })
510}
511
512#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
513pub enum LetterboxRounding {
514    #[default]
515    Floor,
516    Nearest,
517}
518
519pub fn letterbox_with(
520    layout: Size,
521    target: Size,
522    rounding: LetterboxRounding,
523) -> Option<Letterbox> {
524    match rounding {
525        LetterboxRounding::Floor => letterbox(layout, target),
526        LetterboxRounding::Nearest => {
527            letterbox_nearest(layout.width as f32, layout.height as f32, target)
528        }
529    }
530}
531
532pub fn letterbox_nearest(layout_width: f32, layout_height: f32, target: Size) -> Option<Letterbox> {
533    let finite = layout_width.is_finite() && layout_height.is_finite();
534    if !finite
535        || layout_width <= 0.0
536        || layout_height <= 0.0
537        || target.width == 0
538        || target.height == 0
539    {
540        return None;
541    }
542    let target_width = target.width as f32;
543    let target_height = target.height as f32;
544    let scale = (target_width / layout_width).min(target_height / layout_height);
545    let x = ((target_width - layout_width * scale) * 0.5).round();
546    let y = ((target_height - layout_height * scale) * 0.5).round();
547    Some(Letterbox {
548        matrix: affine(scale, scale, x, y),
549        inverse: affine(1.0 / scale, 1.0 / scale, -x / scale, -y / scale),
550        visible: Rect {
551            x,
552            y,
553            width: layout_width * scale,
554            height: layout_height * scale,
555        },
556    })
557}
558
559fn affine(sx: f32, sy: f32, tx: f32, ty: f32) -> [f32; 16] {
560    [
561        sx, 0.0, 0.0, 0.0, 0.0, sy, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, tx, ty, 0.0, 1.0,
562    ]
563}
564
565fn transform(matrix: &[f32; 16], x: f32, y: f32) -> [f32; 2] {
566    [
567        matrix[0] * x + matrix[4] * y + matrix[12],
568        matrix[1] * x + matrix[5] * y + matrix[13],
569    ]
570}
571
572#[derive(Clone, Copy, Debug, PartialEq)]
573pub struct Viewport {
574    pub window: Size,
575    pub render: Size,
576    pub letterbox: Letterbox,
577}
578
579impl Viewport {
580    pub fn render_point(&self, window_x: f32, window_y: f32) -> [f32; 2] {
581        [
582            window_x * self.render.width as f32 / self.window.width as f32,
583            window_y * self.render.height as f32 / self.window.height as f32,
584        ]
585    }
586
587    pub fn pointer_to_layout(&self, window_x: f32, window_y: f32) -> [f32; 2] {
588        let [x, y] = self.render_point(window_x, window_y);
589        self.letterbox.unmap(x, y)
590    }
591
592    pub fn pointer_inside(&self, window_x: f32, window_y: f32) -> bool {
593        let [x, y] = self.render_point(window_x, window_y);
594        self.letterbox.contains(x, y)
595    }
596}
597
598pub fn viewport(window: Size, scale: RenderScale, layout: Size) -> Option<Viewport> {
599    viewport_with(window, scale, layout, LetterboxRounding::Floor)
600}
601
602pub fn viewport_with(
603    window: Size,
604    scale: RenderScale,
605    layout: Size,
606    rounding: LetterboxRounding,
607) -> Option<Viewport> {
608    let render = render_size(window, scale)?;
609    Some(Viewport {
610        window,
611        render,
612        letterbox: letterbox_with(layout, render, rounding)?,
613    })
614}
615
616#[derive(Clone, Copy, Debug, PartialEq, Eq)]
617pub enum FrameCap {
618    Fps30,
619    Fps60,
620    Fps90,
621    Fps120,
622    Uncapped,
623}
624
625impl FrameCap {
626    pub fn fps(self) -> Option<u32> {
627        match self {
628            FrameCap::Fps30 => Some(30),
629            FrameCap::Fps60 => Some(60),
630            FrameCap::Fps90 => Some(90),
631            FrameCap::Fps120 => Some(120),
632            FrameCap::Uncapped => None,
633        }
634    }
635}
636
637#[derive(Clone, Copy, Debug, PartialEq, Eq)]
638pub struct FrameCaps {
639    pub focused: FrameCap,
640    pub background: FrameCap,
641    pub pause_when_minimized: bool,
642}
643
644impl FrameCaps {
645    pub fn new(focused: FrameCap, background: FrameCap) -> Self {
646        Self {
647            focused,
648            background,
649            pause_when_minimized: true,
650        }
651    }
652}
653
654#[derive(Clone, Copy, Debug, PartialEq, Eq)]
655pub enum Attention {
656    Focused(bool),
657    Occluded(bool),
658    Minimized(bool),
659}
660
661pub trait Clock {
662    fn now_ns(&self) -> u64;
663}
664
665#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
666pub struct FakeClock {
667    pub now_ns: u64,
668}
669
670impl Clock for FakeClock {
671    fn now_ns(&self) -> u64 {
672        self.now_ns
673    }
674}
675
676pub struct SystemClock {
677    start: std::time::Instant,
678}
679
680impl SystemClock {
681    pub fn new() -> Self {
682        Self::default()
683    }
684
685    pub fn instant(&self, ns: u64) -> std::time::Instant {
686        self.start + std::time::Duration::from_nanos(ns)
687    }
688}
689
690impl Default for SystemClock {
691    fn default() -> Self {
692        Self {
693            start: std::time::Instant::now(),
694        }
695    }
696}
697
698impl Clock for SystemClock {
699    fn now_ns(&self) -> u64 {
700        u64::try_from(self.start.elapsed().as_nanos()).unwrap_or(u64::MAX)
701    }
702}
703
704#[derive(Clone, Copy, Debug, PartialEq, Eq)]
705pub enum Pace {
706    Start { next_ns: Option<u64> },
707    Wait { until_ns: u64 },
708    Paused,
709}
710
711#[derive(Clone, Copy, Debug)]
712enum Schedule {
713    Fresh,
714    Hold,
715    Open,
716    Grid {
717        origin_ns: u64,
718        index: u64,
719        fps: u32,
720    },
721}
722
723#[derive(Clone, Debug)]
724pub struct FramePacer {
725    caps: FrameCaps,
726    focused: bool,
727    occluded: bool,
728    minimized: bool,
729    vrr_millihertz: Option<u32>,
730    schedule: Schedule,
731}
732
733impl FramePacer {
734    pub fn new(caps: FrameCaps) -> Self {
735        Self {
736            caps,
737            focused: true,
738            occluded: false,
739            minimized: false,
740            vrr_millihertz: None,
741            schedule: Schedule::Fresh,
742        }
743    }
744
745    pub fn set_caps(&mut self, caps: FrameCaps) {
746        self.caps = caps;
747    }
748
749    pub fn set_vrr(&mut self, refresh_millihertz: Option<u32>) {
750        self.vrr_millihertz = refresh_millihertz;
751    }
752
753    pub fn on(&mut self, event: Attention) {
754        match event {
755            Attention::Focused(focused) => self.focused = focused,
756            Attention::Occluded(occluded) => self.occluded = occluded,
757            Attention::Minimized(minimized) => self.minimized = minimized,
758        }
759    }
760
761    pub fn effective_fps(&self) -> Option<u32> {
762        let cap = if self.focused && !self.occluded {
763            self.caps.focused
764        } else {
765            self.caps.background
766        };
767        paced_fps(cap, self.vrr_millihertz)
768    }
769
770    pub fn poll(&mut self, clock: &impl Clock) -> Pace {
771        let now = clock.now_ns();
772        if self.caps.pause_when_minimized && self.minimized {
773            self.schedule = Schedule::Hold;
774            return Pace::Paused;
775        }
776        match self.effective_fps() {
777            None => {
778                self.schedule = Schedule::Open;
779                Pace::Start { next_ns: None }
780            }
781            Some(0) => {
782                self.schedule = Schedule::Hold;
783                Pace::Paused
784            }
785            Some(fps) => self.poll_grid(now, fps),
786        }
787    }
788
789    fn poll_grid(&mut self, now: u64, fps: u32) -> Pace {
790        let (origin_ns, index) = match self.schedule {
791            Schedule::Grid {
792                origin_ns,
793                index,
794                fps: scheduled,
795            } if scheduled == fps => (origin_ns, index),
796            _ => (now, 0),
797        };
798        let due = slot(origin_ns, index, fps);
799        if now < due {
800            return Pace::Wait { until_ns: due };
801        }
802        let next = first_slot_after(origin_ns, now, fps).max(index.saturating_add(1));
803        self.schedule = Schedule::Grid {
804            origin_ns,
805            index: next,
806            fps,
807        };
808        Pace::Start {
809            next_ns: Some(slot(origin_ns, next, fps)),
810        }
811    }
812}
813
814pub fn vrr_limit_fps(refresh_millihertz: u32) -> Option<u32> {
815    let fps = refresh_millihertz.saturating_sub(VRR_HEADROOM_MHZ) / 1_000;
816    (fps > 0).then_some(fps)
817}
818
819pub fn paced_fps(cap: FrameCap, vrr_millihertz: Option<u32>) -> Option<u32> {
820    let Some(limit) = vrr_millihertz.and_then(vrr_limit_fps) else {
821        return cap.fps();
822    };
823    Some(cap.fps().map_or(limit, |fps| fps.min(limit)))
824}
825
826fn slot(origin_ns: u64, index: u64, fps: u32) -> u64 {
827    debug_assert!(fps > 0);
828    let step = u128::from(index) * u128::from(NS_PER_SEC) / u128::from(fps);
829    origin_ns.saturating_add(u64::try_from(step).unwrap_or(u64::MAX))
830}
831
832fn first_slot_after(origin_ns: u64, now_ns: u64, fps: u32) -> u64 {
833    debug_assert!(fps > 0);
834    if now_ns <= origin_ns {
835        return 1;
836    }
837    let need = u128::from(now_ns - origin_ns) + 1;
838    let ns = u128::from(NS_PER_SEC);
839    let prod = need.saturating_mul(u128::from(fps));
840    let index = prod.div_ceil(ns);
841    u64::try_from(index).unwrap_or(u64::MAX).max(1)
842}
843
844#[cfg(any(feature = "window", test))]
845mod glue {
846    use super::{
847        Attention, DisplayMode, Fallback, FullscreenPlan, Host, ModePlan, Monitor, Monitors,
848        PlanError, PlatformCaps, Point, Size, VideoMode, WindowOps, apply_plan, plan_display_mode,
849    };
850    use winit::dpi::{PhysicalPosition, PhysicalSize};
851    use winit::monitor::MonitorHandle;
852    use winit::window::{Fullscreen, Window};
853
854    pub fn refresh_millihertz(monitor: &MonitorHandle) -> Option<u32> {
855        monitor.refresh_rate_millihertz()
856    }
857
858    pub fn minimized(window: &Window) -> bool {
859        window.is_minimized().unwrap_or(false)
860    }
861
862    pub fn attention(event: &winit::event::WindowEvent) -> Option<Attention> {
863        match event {
864            winit::event::WindowEvent::Focused(focused) => Some(Attention::Focused(*focused)),
865            winit::event::WindowEvent::Occluded(occluded) => Some(Attention::Occluded(*occluded)),
866            _ => None,
867        }
868    }
869
870    pub fn host(window: &Window) -> Host {
871        #[cfg(target_os = "windows")]
872        {
873            let _ = window;
874            Host::Windows
875        }
876        #[cfg(target_os = "macos")]
877        {
878            let _ = window;
879            Host::Macos
880        }
881        #[cfg(any(target_os = "android", target_os = "ios", target_arch = "wasm32"))]
882        {
883            let _ = window;
884            Host::Other
885        }
886        #[cfg(any(
887            target_os = "linux",
888            target_os = "freebsd",
889            target_os = "dragonfly",
890            target_os = "openbsd",
891            target_os = "netbsd",
892        ))]
893        {
894            if wayland(window) {
895                Host::Wayland
896            } else {
897                Host::X11
898            }
899        }
900        #[cfg(not(any(
901            target_os = "windows",
902            target_os = "macos",
903            target_os = "android",
904            target_os = "ios",
905            target_arch = "wasm32",
906            target_os = "linux",
907            target_os = "freebsd",
908            target_os = "dragonfly",
909            target_os = "openbsd",
910            target_os = "netbsd",
911        )))]
912        {
913            let _ = window;
914            Host::Other
915        }
916    }
917
918    #[cfg(any(
919        target_os = "linux",
920        target_os = "freebsd",
921        target_os = "dragonfly",
922        target_os = "openbsd",
923        target_os = "netbsd",
924    ))]
925    fn wayland(window: &Window) -> bool {
926        use winit::platform::wayland::WindowExtWayland;
927        window.xdg_toplevel().is_some()
928    }
929
930    pub fn monitors(window: &Window) -> Monitors {
931        let handles: Vec<MonitorHandle> = window.available_monitors().collect();
932        listed(window, &handles)
933    }
934
935    fn listed(window: &Window, handles: &[MonitorHandle]) -> Monitors {
936        Monitors {
937            current: index_of(handles, window.current_monitor().as_ref()),
938            primary: index_of(handles, window.primary_monitor().as_ref()),
939            list: handles.iter().map(describe).collect(),
940        }
941    }
942
943    pub fn placement(window: &Window) -> Option<(Option<String>, Point)> {
944        let monitor = window.current_monitor()?;
945        let outer = window.outer_position().ok()?;
946        let origin = monitor.position();
947        Some((
948            monitor.name(),
949            Point {
950                x: outer.x - origin.x,
951                y: outer.y - origin.y,
952            },
953        ))
954    }
955
956    pub fn apply_display_mode(window: &Window, mode: &DisplayMode) -> Result<ModePlan, PlanError> {
957        let handles: Vec<MonitorHandle> = window.available_monitors().collect();
958        let monitors = listed(window, &handles);
959        let platform = PlatformCaps {
960            exclusive: host(window).supports_exclusive(),
961        };
962        let plan = plan_display_mode(mode, platform, &monitors)?;
963        let mut ops = WinitOps {
964            window,
965            handles: &handles,
966            missed: false,
967        };
968        apply_plan(&plan, &mut ops);
969        Ok(ops.finish(plan))
970    }
971
972    fn index_of(handles: &[MonitorHandle], monitor: Option<&MonitorHandle>) -> Option<usize> {
973        monitor.and_then(|monitor| handles.iter().position(|handle| handle == monitor))
974    }
975
976    fn describe(handle: &MonitorHandle) -> Monitor {
977        let position = handle.position();
978        let size = handle.size();
979        Monitor {
980            name: handle.name(),
981            position: Point {
982                x: position.x,
983                y: position.y,
984            },
985            size: Size {
986                width: size.width,
987                height: size.height,
988            },
989            refresh_millihertz: refresh_millihertz(handle),
990            video_modes: handle
991                .video_modes()
992                .map(|mode| {
993                    let size = mode.size();
994                    VideoMode {
995                        size: Size {
996                            width: size.width,
997                            height: size.height,
998                        },
999                        bit_depth: mode.bit_depth(),
1000                        refresh_millihertz: mode.refresh_rate_millihertz(),
1001                    }
1002                })
1003                .collect(),
1004        }
1005    }
1006
1007    struct WinitOps<'a> {
1008        window: &'a Window,
1009        handles: &'a [MonitorHandle],
1010        missed: bool,
1011    }
1012
1013    impl WindowOps for WinitOps<'_> {
1014        fn set_fullscreen(&mut self, fullscreen: &FullscreenPlan) {
1015            match fullscreen {
1016                FullscreenPlan::Windowed => self.window.set_fullscreen(None),
1017                FullscreenPlan::Borderless { monitor } => {
1018                    let handle = self.handles.get(*monitor).cloned();
1019                    self.window
1020                        .set_fullscreen(Some(Fullscreen::Borderless(handle)));
1021                }
1022                FullscreenPlan::Exclusive { monitor, mode } => {
1023                    let video = self
1024                        .handles
1025                        .get(*monitor)
1026                        .and_then(|handle| find_handle(handle, mode));
1027                    if let Some(video) = video {
1028                        self.window
1029                            .set_fullscreen(Some(Fullscreen::Exclusive(video)));
1030                    } else {
1031                        self.missed = true;
1032                        let handle = self.handles.get(*monitor).cloned();
1033                        self.window
1034                            .set_fullscreen(Some(Fullscreen::Borderless(handle)));
1035                    }
1036                }
1037            }
1038        }
1039
1040        fn set_decorations(&mut self, decorations: bool) {
1041            self.window.set_decorations(decorations);
1042        }
1043
1044        fn set_resizable(&mut self, resizable: bool) {
1045            self.window.set_resizable(resizable);
1046        }
1047
1048        fn request_inner_size(&mut self, size: Size) {
1049            let _ = self
1050                .window
1051                .request_inner_size(PhysicalSize::new(size.width, size.height));
1052        }
1053
1054        fn set_outer_position(&mut self, position: Point) {
1055            self.window
1056                .set_outer_position(PhysicalPosition::new(position.x, position.y));
1057        }
1058    }
1059
1060    impl WinitOps<'_> {
1061        fn finish(self, mut plan: ModePlan) -> ModePlan {
1062            if self.missed
1063                && let FullscreenPlan::Exclusive { monitor, .. } = plan.fullscreen
1064            {
1065                plan.fullscreen = FullscreenPlan::Borderless { monitor };
1066                if plan.fallback.is_none() {
1067                    plan.fallback = Some(Fallback::VideoModeMissing);
1068                }
1069            }
1070            plan
1071        }
1072    }
1073
1074    fn find_handle(
1075        monitor: &MonitorHandle,
1076        mode: &VideoMode,
1077    ) -> Option<winit::monitor::VideoModeHandle> {
1078        monitor.video_modes().find(|candidate| {
1079            let size = candidate.size();
1080            size.width == mode.size.width
1081                && size.height == mode.size.height
1082                && candidate.bit_depth() == mode.bit_depth
1083                && candidate.refresh_rate_millihertz() == mode.refresh_millihertz
1084        })
1085    }
1086}
1087
1088#[cfg(any(feature = "window", test))]
1089pub use glue::{
1090    apply_display_mode, attention, host, minimized, monitors, placement, refresh_millihertz,
1091};
1092
1093#[cfg(test)]
1094mod tests {
1095    use super::*;
1096
1097    const WAYLAND: PlatformCaps = PlatformCaps { exclusive: false };
1098    const X11: PlatformCaps = PlatformCaps { exclusive: true };
1099
1100    fn size(width: u32, height: u32) -> Size {
1101        Size { width, height }
1102    }
1103
1104    fn video(width: u32, height: u32, refresh: u32, depth: u16) -> VideoMode {
1105        VideoMode {
1106            size: size(width, height),
1107            bit_depth: depth,
1108            refresh_millihertz: refresh,
1109        }
1110    }
1111
1112    fn desk() -> Monitors {
1113        Monitors {
1114            list: vec![
1115                Monitor {
1116                    name: Some("primary".into()),
1117                    position: Point { x: 0, y: 0 },
1118                    size: size(1920, 1080),
1119                    refresh_millihertz: Some(60_000),
1120                    video_modes: vec![video(1920, 1080, 60_000, 32)],
1121                },
1122                Monitor {
1123                    name: Some("side".into()),
1124                    position: Point { x: 1920, y: 0 },
1125                    size: size(2560, 1440),
1126                    refresh_millihertz: Some(144_000),
1127                    video_modes: vec![
1128                        video(2560, 1440, 144_000, 24),
1129                        video(2560, 1440, 144_000, 32),
1130                        video(1920, 1080, 60_000, 32),
1131                    ],
1132                },
1133            ],
1134            primary: Some(0),
1135            current: Some(1),
1136        }
1137    }
1138
1139    #[derive(Default)]
1140    struct Record {
1141        calls: Vec<Call>,
1142    }
1143
1144    #[derive(Debug, PartialEq)]
1145    enum Call {
1146        Fullscreen(FullscreenPlan),
1147        Decorations(bool),
1148        Size(Size),
1149        Position(Point),
1150    }
1151
1152    impl WindowOps for Record {
1153        fn set_fullscreen(&mut self, fullscreen: &FullscreenPlan) {
1154            self.calls.push(Call::Fullscreen(*fullscreen));
1155        }
1156
1157        fn set_decorations(&mut self, decorations: bool) {
1158            self.calls.push(Call::Decorations(decorations));
1159        }
1160
1161        fn request_inner_size(&mut self, size: Size) {
1162            self.calls.push(Call::Size(size));
1163        }
1164
1165        fn set_outer_position(&mut self, position: Point) {
1166            self.calls.push(Call::Position(position));
1167        }
1168    }
1169
1170    fn applied(plan: &ModePlan) -> Vec<Call> {
1171        let mut record = Record::default();
1172        apply_plan(plan, &mut record);
1173        record.calls
1174    }
1175
1176    #[test]
1177    fn windowed_and_borderless_windowed_keep_their_size() {
1178        let monitors = desk();
1179        let place = Some(Point { x: 40, y: 20 });
1180        let windowed = plan_display_mode(
1181            &DisplayMode::Windowed {
1182                size: size(1280, 720),
1183                position: place,
1184            },
1185            X11,
1186            &monitors,
1187        )
1188        .unwrap();
1189        assert_eq!(windowed.fullscreen, FullscreenPlan::Windowed);
1190        assert!(windowed.decorations);
1191        assert_eq!(windowed.fallback, None);
1192        assert_eq!(windowed.monitor, Some(1));
1193        assert_eq!(
1194            applied(&windowed),
1195            vec![
1196                Call::Fullscreen(FullscreenPlan::Windowed),
1197                Call::Decorations(true),
1198                Call::Position(Point { x: 40, y: 20 }),
1199                Call::Size(size(1280, 720)),
1200            ]
1201        );
1202        let borderless = plan_display_mode(
1203            &DisplayMode::BorderlessWindowed {
1204                size: size(1600, 900),
1205                position: None,
1206            },
1207            WAYLAND,
1208            &monitors,
1209        )
1210        .unwrap();
1211        assert!(!borderless.decorations);
1212        assert_eq!(borderless.fallback, None);
1213        assert_eq!(
1214            applied(&borderless),
1215            vec![
1216                Call::Fullscreen(FullscreenPlan::Windowed),
1217                Call::Decorations(false),
1218                Call::Size(size(1600, 900)),
1219            ]
1220        );
1221        assert!(matches!(
1222            plan_display_mode(
1223                &DisplayMode::Windowed {
1224                    size: size(0, 10),
1225                    position: None,
1226                },
1227                X11,
1228                &monitors,
1229            ),
1230            Err(PlanError::EmptySize)
1231        ));
1232    }
1233
1234    #[test]
1235    fn fullscreen_chooses_a_monitor_and_exclusive_falls_back() {
1236        let monitors = desk();
1237        let borderless = plan_display_mode(
1238            &DisplayMode::BorderlessFullscreen {
1239                monitor: MonitorChoice::Name("primary".into()),
1240            },
1241            WAYLAND,
1242            &monitors,
1243        )
1244        .unwrap();
1245        assert_eq!(
1246            borderless.fullscreen,
1247            FullscreenPlan::Borderless { monitor: 0 }
1248        );
1249        assert_eq!(borderless.fallback, None);
1250        assert_eq!(
1251            applied(&borderless),
1252            vec![
1253                Call::Decorations(false),
1254                Call::Fullscreen(FullscreenPlan::Borderless { monitor: 0 }),
1255            ]
1256        );
1257
1258        let mode = VideoModeChoice::SizeRefresh {
1259            size: size(2560, 1440),
1260            refresh_millihertz: 144_000,
1261        };
1262        let exclusive = plan_display_mode(
1263            &DisplayMode::ExclusiveFullscreen {
1264                monitor: MonitorChoice::Current,
1265                video_mode: mode,
1266            },
1267            X11,
1268            &monitors,
1269        )
1270        .unwrap();
1271        assert_eq!(
1272            exclusive.fullscreen,
1273            FullscreenPlan::Exclusive {
1274                monitor: 1,
1275                mode: video(2560, 1440, 144_000, 32),
1276            }
1277        );
1278        assert_eq!(exclusive.fallback, None);
1279
1280        let exact = plan_display_mode(
1281            &DisplayMode::ExclusiveFullscreen {
1282                monitor: MonitorChoice::Index(1),
1283                video_mode: VideoModeChoice::Exact(video(1920, 1080, 60_000, 32)),
1284            },
1285            X11,
1286            &monitors,
1287        )
1288        .unwrap();
1289        assert_eq!(
1290            exact.fullscreen,
1291            FullscreenPlan::Exclusive {
1292                monitor: 1,
1293                mode: video(1920, 1080, 60_000, 32),
1294            }
1295        );
1296        let depth = plan_display_mode(
1297            &DisplayMode::ExclusiveFullscreen {
1298                monitor: MonitorChoice::Index(1),
1299                video_mode: VideoModeChoice::Exact(video(2560, 1440, 144_000, 24)),
1300            },
1301            X11,
1302            &monitors,
1303        )
1304        .unwrap();
1305        assert_eq!(
1306            depth.fullscreen,
1307            FullscreenPlan::Exclusive {
1308                monitor: 1,
1309                mode: video(2560, 1440, 144_000, 24),
1310            }
1311        );
1312
1313        let wayland = plan_display_mode(
1314            &DisplayMode::ExclusiveFullscreen {
1315                monitor: MonitorChoice::Primary,
1316                video_mode: VideoModeChoice::Exact(video(1920, 1080, 60_000, 32)),
1317            },
1318            WAYLAND,
1319            &monitors,
1320        )
1321        .unwrap();
1322        assert_eq!(
1323            wayland.fullscreen,
1324            FullscreenPlan::Borderless { monitor: 0 }
1325        );
1326        assert_eq!(wayland.fallback, Some(Fallback::ExclusiveUnsupported));
1327        assert!(
1328            applied(&wayland)
1329                .contains(&Call::Fullscreen(FullscreenPlan::Borderless { monitor: 0 }))
1330        );
1331
1332        let missing_mode = plan_display_mode(
1333            &DisplayMode::ExclusiveFullscreen {
1334                monitor: MonitorChoice::Primary,
1335                video_mode: VideoModeChoice::Exact(video(1280, 720, 60_000, 32)),
1336            },
1337            X11,
1338            &monitors,
1339        )
1340        .unwrap();
1341        assert_eq!(
1342            missing_mode.fullscreen,
1343            FullscreenPlan::Borderless { monitor: 0 }
1344        );
1345        assert_eq!(missing_mode.fallback, Some(Fallback::VideoModeMissing));
1346
1347        let missing_monitor = plan_display_mode(
1348            &DisplayMode::BorderlessFullscreen {
1349                monitor: MonitorChoice::Name("gone".into()),
1350            },
1351            X11,
1352            &monitors,
1353        )
1354        .unwrap();
1355        assert_eq!(
1356            missing_monitor.fullscreen,
1357            FullscreenPlan::Borderless { monitor: 1 }
1358        );
1359        assert_eq!(missing_monitor.fallback, Some(Fallback::MonitorMissing));
1360
1361        let missing_index = plan_display_mode(
1362            &DisplayMode::ExclusiveFullscreen {
1363                monitor: MonitorChoice::Index(9),
1364                video_mode: VideoModeChoice::Exact(video(1920, 1080, 60_000, 32)),
1365            },
1366            X11,
1367            &monitors,
1368        )
1369        .unwrap();
1370        assert_eq!(missing_index.monitor, Some(1));
1371        assert_eq!(missing_index.fallback, Some(Fallback::MonitorMissing));
1372        assert!(matches!(
1373            missing_index.fullscreen,
1374            FullscreenPlan::Exclusive { monitor: 1, .. }
1375        ));
1376
1377        assert!(matches!(
1378            plan_display_mode(
1379                &DisplayMode::BorderlessFullscreen {
1380                    monitor: MonitorChoice::Current,
1381                },
1382                X11,
1383                &Monitors {
1384                    list: Vec::new(),
1385                    primary: None,
1386                    current: None,
1387                },
1388            ),
1389            Err(PlanError::NoMonitor)
1390        ));
1391    }
1392
1393    #[test]
1394    fn present_mode_follows_the_preference_and_falls_back_to_fifo() {
1395        let fifo = PresentPreference::default();
1396        assert_eq!(fifo.modes, vec![wgpu::PresentMode::Fifo]);
1397        assert_eq!(fifo.desired_maximum_frame_latency, 2);
1398        let game = PresentPreference::game();
1399        assert_eq!(
1400            game.modes,
1401            vec![
1402                wgpu::PresentMode::Mailbox,
1403                wgpu::PresentMode::FifoRelaxed,
1404                wgpu::PresentMode::Fifo,
1405            ]
1406        );
1407        let available = [
1408            wgpu::PresentMode::Fifo,
1409            wgpu::PresentMode::Immediate,
1410            wgpu::PresentMode::FifoRelaxed,
1411        ];
1412        let choice = choose_present(&game, &available);
1413        assert_eq!(choice.selected, wgpu::PresentMode::FifoRelaxed);
1414        assert!(!choice.fell_back);
1415        assert_eq!(choice.desired_maximum_frame_latency, 2);
1416
1417        let mailbox_only = PresentPreference {
1418            modes: vec![wgpu::PresentMode::Mailbox],
1419            desired_maximum_frame_latency: 0,
1420        };
1421        let fell = choose_present(&mailbox_only, &[wgpu::PresentMode::Fifo]);
1422        assert_eq!(fell.selected, wgpu::PresentMode::Fifo);
1423        assert!(fell.fell_back);
1424        assert_eq!(fell.desired_maximum_frame_latency, 1);
1425
1426        let empty = choose_present(&mailbox_only, &[]);
1427        assert_eq!(empty.selected, wgpu::PresentMode::Fifo);
1428        assert!(empty.fell_back);
1429
1430        let asked = choose_present(
1431            &PresentPreference::fifo(),
1432            &[wgpu::PresentMode::Mailbox, wgpu::PresentMode::Fifo],
1433        );
1434        assert_eq!(asked.selected, wgpu::PresentMode::Fifo);
1435        assert!(!asked.fell_back);
1436
1437        assert_eq!(
1438            Host::Windows.vrr_present_modes(),
1439            PresentPreference::game().modes.as_slice()
1440        );
1441        assert_eq!(
1442            Host::Wayland.vrr_present_modes(),
1443            &[wgpu::PresentMode::Mailbox, wgpu::PresentMode::Fifo]
1444        );
1445        assert_eq!(
1446            Host::X11.vrr_present_modes(),
1447            &[
1448                wgpu::PresentMode::Mailbox,
1449                wgpu::PresentMode::Fifo,
1450                wgpu::PresentMode::FifoRelaxed,
1451            ]
1452        );
1453        assert_eq!(
1454            Host::Macos.vrr_present_modes(),
1455            &[wgpu::PresentMode::Mailbox, wgpu::PresentMode::Fifo]
1456        );
1457        assert_eq!(Host::Other.vrr_present_modes(), &[wgpu::PresentMode::Fifo]);
1458        assert!(Host::X11.supports_exclusive());
1459        assert!(Host::Windows.supports_exclusive());
1460        assert!(Host::Macos.supports_exclusive());
1461        assert!(!Host::Wayland.supports_exclusive());
1462        assert!(!Host::Other.supports_exclusive());
1463        assert_eq!(
1464            PresentPreference::vrr(Host::Wayland).desired_maximum_frame_latency,
1465            1
1466        );
1467    }
1468
1469    fn affine(sx: f32, sy: f32, tx: f32, ty: f32) -> [f32; 16] {
1470        [
1471            sx, 0.0, 0.0, 0.0, 0.0, sy, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, tx, ty, 0.0, 1.0,
1472        ]
1473    }
1474
1475    fn assert_round_trip(letter: &Letterbox, x: f32, y: f32) {
1476        let [px, py] = letter.map(x, y);
1477        let [lx, ly] = letter.unmap(px, py);
1478        assert!((lx - x).abs() < 1e-3 && (ly - y).abs() < 1e-3);
1479    }
1480
1481    #[test]
1482    fn nearest_rounding_keeps_one_scale_and_rounds_the_offset() {
1483        let layout = LAYOUT_UNITS;
1484        for target in [
1485            size(3840, 2160),
1486            size(1920, 1080),
1487            size(2520, 1080),
1488            size(1440, 1080),
1489        ] {
1490            assert_eq!(
1491                letterbox_with(layout, target, LetterboxRounding::Nearest),
1492                letterbox(layout, target)
1493            );
1494        }
1495        let deck = letterbox_with(layout, size(1280, 800), LetterboxRounding::Nearest).unwrap();
1496        assert_eq!(deck.map(0.0, 0.0), [0.0, 40.0]);
1497        assert_eq!(deck.matrix[0], deck.matrix[5]);
1498        assert_round_trip(&deck, 1919.0, 1079.0);
1499        let odd =
1500            letterbox_with(size(100, 100), size(201, 100), LetterboxRounding::Nearest).unwrap();
1501        assert_eq!(odd.visible.x, 51.0);
1502        assert_eq!(
1503            letterbox(size(100, 100), size(201, 100)).unwrap().visible.x,
1504            50.0
1505        );
1506        let laptop = letterbox_with(layout, size(1366, 768), LetterboxRounding::Nearest).unwrap();
1507        assert_eq!(laptop.matrix[0], laptop.matrix[5]);
1508        assert_eq!(laptop.map(0.0, 0.0), [0.0, 0.0]);
1509        let fractional = letterbox_nearest(1920.5, 1080.0, size(1280, 800)).unwrap();
1510        assert!(fractional.map(1920.5, 1080.0)[0] <= 1280.0);
1511        assert!(letterbox_nearest(0.0, 1.0, size(1, 1)).is_none());
1512        assert!(letterbox_nearest(f32::NAN, 1.0, size(1, 1)).is_none());
1513        let window = size(2560, 1600);
1514        let view = viewport_with(
1515            window,
1516            RenderScale::Native,
1517            layout,
1518            LetterboxRounding::Nearest,
1519        )
1520        .unwrap();
1521        let [x, y] = view.letterbox.map(960.0, 540.0);
1522        let back = view.pointer_to_layout(
1523            x * 2560.0 / view.render.width as f32,
1524            y * 1600.0 / view.render.height as f32,
1525        );
1526        assert!((back[0] - 960.0).abs() < 1e-3 && (back[1] - 540.0).abs() < 1e-3);
1527    }
1528
1529    #[test]
1530    fn letterbox_keeps_16_by_9_inside_16_by_9_21_by_9_and_4_by_3() {
1531        let layout = LAYOUT_UNITS;
1532        let same = letterbox(layout, size(1920, 1080)).unwrap();
1533        assert_eq!(same.matrix, affine(1.0, 1.0, 0.0, 0.0));
1534        assert_eq!(same.inverse, affine(1.0, 1.0, 0.0, 0.0));
1535        assert_eq!(
1536            same.visible,
1537            Rect {
1538                x: 0.0,
1539                y: 0.0,
1540                width: 1920.0,
1541                height: 1080.0,
1542            }
1543        );
1544        assert_eq!(same.map(960.0, 540.0), [960.0, 540.0]);
1545        assert!(same.contains(0.0, 0.0));
1546        assert!(!same.contains(1920.0, 0.0));
1547
1548        let wide = letterbox(layout, size(2520, 1080)).unwrap();
1549        assert_eq!(wide.matrix, affine(1.0, 1.0, 300.0, 0.0));
1550        assert_eq!(wide.inverse, affine(1.0, 1.0, -300.0, 0.0));
1551        assert_eq!(wide.map(0.0, 0.0), [300.0, 0.0]);
1552        assert_eq!(wide.map(960.0, 540.0), [1260.0, 540.0]);
1553        assert!(!wide.contains(10.0, 10.0));
1554        assert!(wide.contains(300.0, 0.0));
1555        assert!(wide.contains(2219.0, 1079.0));
1556        assert!(!wide.contains(2220.0, 0.0));
1557        assert_round_trip(&wide, 1.0, 1.0);
1558        assert_round_trip(&wide, 1919.0, 1079.0);
1559
1560        let tall = letterbox(layout, size(1440, 1080)).unwrap();
1561        assert_eq!(tall.matrix, affine(0.75, 0.75, 0.0, 135.0));
1562        assert_eq!(tall.inverse, affine(1.0 / 0.75, 1.0 / 0.75, 0.0, -180.0));
1563        assert_eq!(tall.map(960.0, 540.0), [720.0, 540.0]);
1564        assert_eq!(tall.unmap(720.0, 540.0), [960.0, 540.0]);
1565        assert!(!tall.contains(10.0, 10.0));
1566        assert!(tall.contains(0.0, 135.0));
1567        assert!(!tall.contains(0.0, 135.0 + 810.0));
1568        assert_round_trip(&tall, 0.0, 0.0);
1569        assert_round_trip(&tall, 1920.0, 1080.0);
1570
1571        let odd = letterbox(size(100, 100), size(201, 100)).unwrap();
1572        assert_eq!(odd.visible.x, 50.0);
1573        assert_eq!(odd.visible.width, 100.0);
1574        assert_eq!(odd.visible.y, 0.0);
1575        assert!(letterbox(size(0, 10), size(10, 10)).is_none());
1576    }
1577
1578    #[test]
1579    fn render_scale_is_independent_of_the_window_and_the_pointer_maps_back() {
1580        assert_eq!(
1581            render_size(size(1920, 1080), RenderScale::Native),
1582            Some(size(1920, 1080))
1583        );
1584        assert_eq!(
1585            render_size(size(1920, 1080), RenderScale::Factor(0.5)),
1586            Some(size(960, 540))
1587        );
1588        assert_eq!(
1589            render_size(size(1919, 1079), RenderScale::Factor(0.5)),
1590            Some(size(960, 540))
1591        );
1592        assert_eq!(
1593            render_size(size(800, 600), RenderScale::Size(size(320, 180))),
1594            Some(size(320, 180))
1595        );
1596        assert!(render_size(size(1920, 1080), RenderScale::Factor(0.0)).is_none());
1597        assert!(render_size(size(1920, 1080), RenderScale::Factor(-1.0)).is_none());
1598        assert!(render_size(size(1920, 1080), RenderScale::Factor(f32::NAN)).is_none());
1599        assert!(render_size(size(0, 10), RenderScale::Native).is_none());
1600
1601        let view = viewport(size(2520, 1080), RenderScale::Factor(0.5), LAYOUT_UNITS).unwrap();
1602        assert_eq!(view.render, size(1260, 540));
1603        assert_eq!(view.pointer_to_layout(1260.0, 540.0), [960.0, 540.0]);
1604        assert!(view.pointer_inside(300.0, 0.0));
1605        assert!(!view.pointer_inside(299.0, 0.0));
1606        let [x, y] = view.pointer_to_layout(300.0, 0.0);
1607        assert!((x - 0.0).abs() < 1e-3 && (y - 0.0).abs() < 1e-3);
1608    }
1609
1610    fn take(pacer: &mut FramePacer, clock: &mut FakeClock) -> u64 {
1611        for _ in 0..4 {
1612            match pacer.poll(clock) {
1613                Pace::Start { .. } => return clock.now_ns,
1614                Pace::Wait { until_ns } => clock.now_ns = until_ns,
1615                Pace::Paused => panic!("paused"),
1616            }
1617        }
1618        panic!("no start");
1619    }
1620
1621    #[test]
1622    fn the_absolute_grid_does_not_drift() {
1623        assert_eq!(slot(0, 0, 60), 0);
1624        assert_eq!(slot(0, 1, 60), NS_PER_SEC / 60);
1625        assert_eq!(slot(0, 60, 60), NS_PER_SEC);
1626        assert_eq!(first_slot_after(0, 0, 60), 1);
1627        assert_eq!(first_slot_after(0, NS_PER_SEC / 60, 60), 2);
1628        assert_eq!(first_slot_after(0, 10 * NS_PER_SEC / 60, 60), 11);
1629        assert_eq!(first_slot_after(0, 10 * NS_PER_SEC / 60 + 10, 60), 11);
1630        assert_eq!(first_slot_after(0, NS_PER_SEC, 30), 31);
1631    }
1632
1633    #[test]
1634    fn a_thousand_frames_at_each_cap_stay_within_one_tick() {
1635        for cap in [
1636            FrameCap::Fps30,
1637            FrameCap::Fps60,
1638            FrameCap::Fps90,
1639            FrameCap::Fps120,
1640        ] {
1641            let fps = u64::from(cap.fps().unwrap());
1642            let mut pacer = FramePacer::new(FrameCaps::new(cap, cap));
1643            let mut clock = FakeClock::default();
1644            for frame in 0..1_000 {
1645                let start = take(&mut pacer, &mut clock);
1646                let expected = frame * NS_PER_SEC / fps;
1647                assert!(
1648                    start.abs_diff(expected) <= 1,
1649                    "{cap:?} frame {frame} at {start}, grid {expected}"
1650                );
1651            }
1652        }
1653    }
1654
1655    #[test]
1656    fn uncapped_never_waits_and_a_late_frame_returns_to_the_grid() {
1657        let mut pacer = FramePacer::new(FrameCaps::new(FrameCap::Uncapped, FrameCap::Fps30));
1658        let mut clock = FakeClock::default();
1659        for frame in 0..1_000 {
1660            clock.now_ns = frame * 123_456 + (frame % 7) * 89;
1661            assert!(matches!(pacer.poll(&clock), Pace::Start { next_ns: None }));
1662        }
1663
1664        let mut pacer = FramePacer::new(FrameCaps::new(FrameCap::Fps60, FrameCap::Fps60));
1665        let mut clock = FakeClock::default();
1666        assert!(matches!(pacer.poll(&clock), Pace::Start { .. }));
1667        clock.now_ns = 10 * NS_PER_SEC / 60 + 10;
1668        match pacer.poll(&clock) {
1669            Pace::Start {
1670                next_ns: Some(next),
1671            } => {
1672                assert_eq!(next, 11 * NS_PER_SEC / 60);
1673            }
1674            other => panic!("late frame should start once, got {other:?}"),
1675        }
1676        assert!(matches!(
1677            pacer.poll(&clock),
1678            Pace::Wait { until_ns } if until_ns == 11 * NS_PER_SEC / 60
1679        ));
1680        for frame in 11..40 {
1681            let start = take(&mut pacer, &mut clock);
1682            assert_eq!(start, frame * NS_PER_SEC / 60);
1683        }
1684    }
1685
1686    #[test]
1687    fn focus_occlusion_and_minimize_swap_the_schedule() {
1688        let mut pacer = FramePacer::new(FrameCaps::new(FrameCap::Fps60, FrameCap::Fps30));
1689        let mut clock = FakeClock::default();
1690        assert_eq!(take(&mut pacer, &mut clock), 0);
1691        let second = take(&mut pacer, &mut clock);
1692        assert_eq!(second, NS_PER_SEC / 60);
1693
1694        pacer.on(Attention::Occluded(true));
1695        assert_eq!(pacer.effective_fps(), Some(30));
1696        let origin = second;
1697        assert_eq!(take(&mut pacer, &mut clock), origin);
1698        let background = take(&mut pacer, &mut clock);
1699        assert_eq!(background, origin + NS_PER_SEC / 30);
1700        assert!(matches!(pacer.poll(&clock), Pace::Wait { .. }));
1701
1702        pacer.on(Attention::Focused(false));
1703        let held = take(&mut pacer, &mut clock);
1704        assert_eq!(held, origin + 2 * NS_PER_SEC / 30);
1705
1706        pacer.on(Attention::Focused(true));
1707        let still = take(&mut pacer, &mut clock);
1708        assert_eq!(still, origin + 3 * NS_PER_SEC / 30);
1709
1710        pacer.on(Attention::Occluded(false));
1711        assert_eq!(pacer.effective_fps(), Some(60));
1712        assert_eq!(take(&mut pacer, &mut clock), still);
1713        assert_eq!(take(&mut pacer, &mut clock), still + NS_PER_SEC / 60);
1714
1715        pacer.on(Attention::Minimized(true));
1716        assert!(matches!(pacer.poll(&clock), Pace::Paused));
1717        clock.now_ns += 5 * NS_PER_SEC;
1718        assert!(matches!(pacer.poll(&clock), Pace::Paused));
1719        pacer.on(Attention::Minimized(false));
1720        assert!(matches!(pacer.poll(&clock), Pace::Start { .. }));
1721        assert!(matches!(pacer.poll(&clock), Pace::Wait { .. }));
1722
1723        let mut caps = FrameCaps::new(FrameCap::Fps60, FrameCap::Fps30);
1724        caps.pause_when_minimized = false;
1725        let mut running = FramePacer::new(caps);
1726        running.on(Attention::Minimized(true));
1727        assert_eq!(running.effective_fps(), Some(60));
1728        assert!(matches!(running.poll(&clock), Pace::Start { .. }));
1729    }
1730
1731    #[test]
1732    fn vrr_caps_at_refresh_minus_three_and_the_pacer_uses_it() {
1733        let cases = [
1734            (60_000, FrameCap::Fps60, Some(57)),
1735            (60_000, FrameCap::Fps30, Some(30)),
1736            (120_000, FrameCap::Fps120, Some(117)),
1737            (144_000, FrameCap::Fps120, Some(120)),
1738            (144_000, FrameCap::Uncapped, Some(141)),
1739            (165_000, FrameCap::Fps90, Some(90)),
1740            (165_000, FrameCap::Fps120, Some(120)),
1741            (165_000, FrameCap::Uncapped, Some(162)),
1742        ];
1743        for (refresh, cap, expected) in cases {
1744            assert_eq!(paced_fps(cap, Some(refresh)), expected, "{refresh} {cap:?}");
1745        }
1746        assert_eq!(vrr_limit_fps(60_000), Some(57));
1747        assert_eq!(vrr_limit_fps(120_000), Some(117));
1748        assert_eq!(vrr_limit_fps(144_000), Some(141));
1749        assert_eq!(vrr_limit_fps(165_000), Some(162));
1750        assert_eq!(paced_fps(FrameCap::Fps60, None), Some(60));
1751        assert_eq!(paced_fps(FrameCap::Uncapped, None), None);
1752        assert_eq!(vrr_limit_fps(2_000), None);
1753
1754        let mut pacer = FramePacer::new(FrameCaps::new(FrameCap::Fps60, FrameCap::Fps30));
1755        pacer.set_vrr(Some(60_000));
1756        assert_eq!(pacer.effective_fps(), Some(57));
1757        let mut clock = FakeClock::default();
1758        for frame in 0..1_000 {
1759            let start = take(&mut pacer, &mut clock);
1760            assert!(start.abs_diff(frame * NS_PER_SEC / 57) <= 1);
1761        }
1762        pacer.set_vrr(None);
1763        assert_eq!(pacer.effective_fps(), Some(60));
1764        let switched = clock.now_ns;
1765        assert_eq!(take(&mut pacer, &mut clock), switched);
1766        assert_eq!(take(&mut pacer, &mut clock), switched + NS_PER_SEC / 60);
1767    }
1768
1769    #[test]
1770    fn winit_focus_and_occlusion_events_drive_the_pacer() {
1771        let mut pacer = FramePacer::new(FrameCaps::new(FrameCap::Fps120, FrameCap::Fps30));
1772        let blurred = winit::event::WindowEvent::Focused(false);
1773        pacer.on(attention(&blurred).unwrap());
1774        assert_eq!(pacer.effective_fps(), Some(30));
1775        pacer.on(Attention::Focused(true));
1776        let covered = winit::event::WindowEvent::Occluded(true);
1777        pacer.on(attention(&covered).unwrap());
1778        assert_eq!(pacer.effective_fps(), Some(30));
1779        assert!(attention(&winit::event::WindowEvent::RedrawRequested).is_none());
1780        assert!(attention(&winit::event::WindowEvent::CloseRequested).is_none());
1781    }
1782}