Skip to main content

pfx_gpu/screens/
resolution.rs

1use 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}