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}