pfx_gpu/screens/
resolution.rs1use std::collections::VecDeque;
2
3use crate::FrameTimings;
4
5const SMOOTHING: f64 = 0.25;
6const PENDING: usize = 16;
7
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct Budget {
10 pub gpu_ms: f64,
11 pub headroom: f64,
12 pub min_scale: f32,
13 pub max_scale: f32,
14 pub step: f32,
15 pub settle_frames: u32,
16}
17
18impl Budget {
19 pub fn new(gpu_ms: f64) -> Self {
20 Self {
21 gpu_ms,
22 headroom: 0.9,
23 min_scale: 0.5,
24 max_scale: 1.0,
25 step: 0.05,
26 settle_frames: 6,
27 }
28 }
29
30 pub fn validate(&self) -> Result<(), String> {
31 if !(self.gpu_ms.is_finite() && self.gpu_ms > 0.0) {
32 return Err("the GPU budget must be a positive number of milliseconds".into());
33 }
34 if !(self.headroom > 0.0 && self.headroom <= 1.0) {
35 return Err("the headroom must be above 0 and at most 1".into());
36 }
37 if !(self.min_scale > 0.0 && self.min_scale <= self.max_scale && self.max_scale <= 2.0) {
38 return Err("the scales must satisfy 0 < min <= max <= 2".into());
39 }
40 if !(self.step > 0.0 && self.step <= 0.5) {
41 return Err("the step must be above 0 and at most 0.5".into());
42 }
43 Ok(())
44 }
45}
46
47#[derive(Clone, Debug)]
48pub struct DynamicResolution {
49 budget: Budget,
50 scale: f32,
51 cost: Option<f64>,
52 pending: VecDeque<(u64, f32)>,
53 since_change: u32,
54 changes: u32,
55}
56
57impl DynamicResolution {
58 pub fn new(budget: Budget) -> Result<Self, String> {
59 budget.validate()?;
60 Ok(Self {
61 budget,
62 scale: budget.max_scale,
63 cost: None,
64 pending: VecDeque::with_capacity(PENDING),
65 since_change: 0,
66 changes: 0,
67 })
68 }
69
70 pub fn budget(&self) -> Budget {
71 self.budget
72 }
73
74 pub fn scale(&self) -> f32 {
75 self.scale
76 }
77
78 pub fn changes(&self) -> u32 {
79 self.changes
80 }
81
82 pub fn cost_ms_at_full(&self) -> Option<f64> {
83 self.cost
84 }
85
86 pub fn begin(&mut self, frame: u64) -> f32 {
87 self.begin_at(frame, self.scale)
88 }
89
90 pub fn begin_at(&mut self, frame: u64, rendered_scale: f32) -> f32 {
91 if self.pending.len() == PENDING {
92 self.pending.pop_front();
93 }
94 let rendered = if rendered_scale.is_finite() && rendered_scale > 0.0 {
95 rendered_scale
96 } else {
97 self.scale
98 };
99 self.pending.push_back((frame, rendered));
100 self.since_change = self.since_change.saturating_add(1);
101 self.scale
102 }
103
104 pub fn render_size(&self, content: [u32; 2]) -> Option<[u32; 2]> {
105 scaled(content, self.scale)
106 }
107
108 pub fn capacity(&self, content: [u32; 2]) -> Option<[u32; 2]> {
109 scaled(content, self.budget.max_scale)
110 }
111
112 pub fn observe_timings(&mut self, timings: &FrameTimings) -> Option<f32> {
113 timings
114 .gpu_ms()
115 .and_then(|gpu_ms| self.observe(timings.frame, gpu_ms))
116 }
117
118 pub fn observe(&mut self, frame: u64, gpu_ms: f64) -> Option<f32> {
119 if !(gpu_ms.is_finite() && gpu_ms > 0.0) {
120 return None;
121 }
122 let at = self
123 .pending
124 .iter()
125 .position(|(pending, _)| *pending == frame)?;
126 let (_, scale) = self.pending[at];
127 self.pending.drain(..=at);
128 let area = f64::from(scale) * f64::from(scale);
129 let cost = gpu_ms / area;
130 let over = gpu_ms > self.budget.gpu_ms && scale <= self.scale;
131 let smoothed = match self.cost {
132 Some(previous) if over => previous.max(cost),
133 Some(previous) => previous + (cost - previous) * SMOOTHING,
134 None => cost,
135 };
136 self.cost = Some(smoothed);
137 let ideal = self.quantize((self.budget.gpu_ms * self.budget.headroom / smoothed).sqrt());
138 let settled = self.since_change >= self.budget.settle_frames;
139 let predicted = smoothed * f64::from(self.scale) * f64::from(self.scale);
140 let lower = ideal < self.scale && (over || (settled && predicted > self.budget.gpu_ms));
141 let higher = ideal >= self.scale + self.budget.step * 0.999 && settled;
142 if !(lower || higher) {
143 return None;
144 }
145 let next = ideal;
146 self.scale = next;
147 self.since_change = 0;
148 self.changes += 1;
149 Some(next)
150 }
151
152 fn quantize(&self, ideal: f64) -> f32 {
153 let budget = self.budget;
154 let steps = ((ideal - f64::from(budget.min_scale)) / f64::from(budget.step)).floor();
155 let snapped = f64::from(budget.min_scale) + steps.max(0.0) * f64::from(budget.step);
156 let snapped = (snapped * 1e4).round() / 1e4;
157 (snapped as f32).clamp(budget.min_scale, budget.max_scale)
158 }
159}
160
161fn scaled(content: [u32; 2], factor: f32) -> Option<[u32; 2]> {
162 let size = crate::window::render_size(
163 crate::window::Size {
164 width: content[0],
165 height: content[1],
166 },
167 crate::window::RenderScale::Factor(factor),
168 )?;
169 Some([size.width, size.height])
170}