Skip to main content

pfx_gpu/pace/
frames.rs

1use std::path::PathBuf;
2use std::time::{Duration, Instant};
3
4use super::trace::{self, Source, Sources};
5use super::{Frozen, TURN_WALL_MS, gpu_ms, turn_args, unfrozen_ms};
6use crate::PassTiming;
7
8pub const CAP_FPS: f64 = 60.0;
9pub const TURN_AFTER_MS: f64 = 50.0;
10pub const UNCAPPED_VAR: &str = "PFX_UNCAPPED";
11pub const TURN_WALL_VAR: &str = "PFX_TURN_WALL_MS";
12pub const TURN_MS_VAR: &str = "PFX_TURN_MS";
13pub const TURN_VAR: &str = "GPU_QUEUE_TURN";
14
15#[derive(Clone, Copy, Debug, Default, PartialEq)]
16pub struct TurnReport {
17    pub work_ms: f64,
18    pub longest_ms: f64,
19}
20
21impl TurnReport {
22    pub fn args(&self) -> [String; 5] {
23        turn_args(self.work_ms, self.longest_ms)
24    }
25}
26
27pub trait FrameClock {
28    fn now_ms(&mut self) -> f64;
29    fn sleep_ms(&mut self, ms: f64);
30    fn turn(&mut self, report: TurnReport);
31    fn frozen_ms(&mut self) -> f64 {
32        0.0
33    }
34}
35
36pub fn frozen_file_ms(path: Option<&std::path::Path>) -> f64 {
37    match path {
38        Some(path) => Frozen::at(path).ms(),
39        None => 0.0,
40    }
41}
42
43pub trait FrameGpu {
44    fn gpu_ms(&self) -> Option<f64>;
45}
46
47impl FrameGpu for () {
48    fn gpu_ms(&self) -> Option<f64> {
49        None
50    }
51}
52
53impl FrameGpu for Vec<PassTiming> {
54    fn gpu_ms(&self) -> Option<f64> {
55        gpu_ms(self)
56    }
57}
58
59impl FrameGpu for [PassTiming] {
60    fn gpu_ms(&self) -> Option<f64> {
61        gpu_ms(self)
62    }
63}
64
65pub trait Sink {
66    fn progress(&mut self, done: u64, total: u64, fps: f64);
67    fn end(&mut self, ok: bool);
68}
69
70pub struct Tally {
71    sink: Option<Box<dyn Sink>>,
72    planned: u64,
73    done: u64,
74    started_ms: f64,
75}
76
77impl Tally {
78    pub fn none() -> Self {
79        Self {
80            sink: None,
81            planned: 0,
82            done: 0,
83            started_ms: 0.0,
84        }
85    }
86
87    pub fn with_sink(sink: Box<dyn Sink>, planned: u64, now_ms: f64) -> Self {
88        Self {
89            sink: Some(sink),
90            planned,
91            done: 0,
92            started_ms: now_ms,
93        }
94    }
95
96    pub fn frame(&mut self) {
97        self.done += 1;
98    }
99
100    pub fn report(&mut self, now_ms: f64) {
101        let Some(sink) = self.sink.as_mut() else {
102            return;
103        };
104        let elapsed_ms = now_ms - self.started_ms;
105        let fps = if elapsed_ms > 0.0 {
106            self.done as f64 * 1000.0 / elapsed_ms
107        } else {
108            0.0
109        };
110        sink.progress(self.done, self.planned, fps);
111    }
112}
113
114impl Drop for Tally {
115    fn drop(&mut self) {
116        if let Some(sink) = self.sink.as_mut() {
117            sink.end(!std::thread::panicking());
118        }
119    }
120}
121
122pub struct WallClock {
123    start: Instant,
124    frozen: Option<PathBuf>,
125}
126
127impl WallClock {
128    pub fn new() -> Self {
129        Self {
130            start: Instant::now(),
131            frozen: std::env::var_os(super::FROZEN_VAR)
132                .filter(|path| !path.is_empty())
133                .map(PathBuf::from),
134        }
135    }
136}
137
138impl Default for WallClock {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144impl FrameClock for WallClock {
145    fn now_ms(&mut self) -> f64 {
146        self.start.elapsed().as_secs_f64() * 1000.0
147    }
148
149    fn sleep_ms(&mut self, ms: f64) {
150        std::thread::sleep(Duration::from_secs_f64(ms / 1000.0));
151    }
152
153    fn turn(&mut self, report: TurnReport) {
154        if std::env::var_os(TURN_VAR).is_some() {
155            let _ = std::process::Command::new("pgpu")
156                .args(report.args())
157                .stdout(std::process::Stdio::null())
158                .status();
159        }
160    }
161
162    fn frozen_ms(&mut self) -> f64 {
163        frozen_file_ms(self.frozen.as_deref())
164    }
165}
166
167pub fn sleep_to(boundary_ms: f64, now_ms: f64) -> f64 {
168    (boundary_ms - now_ms).max(0.0)
169}
170
171pub fn turn_wall_ms(get: &impl Fn(&str) -> Option<String>) -> f64 {
172    get(TURN_WALL_VAR)
173        .and_then(|value| value.trim().parse::<f64>().ok())
174        .filter(|value| value.is_finite() && *value > 0.0)
175        .unwrap_or(TURN_WALL_MS)
176}
177
178pub fn uncapped(get: &impl Fn(&str) -> Option<String>) -> bool {
179    get(UNCAPPED_VAR).is_some_and(|value| value == "1")
180}
181
182pub struct Pace<C: FrameClock> {
183    pub clock: C,
184    wall_ms: f64,
185    tally: Tally,
186    period_ms: Option<f64>,
187    boundary_ms: f64,
188    pending_ms: f64,
189    longest_ms: f64,
190    last_turn_ms: f64,
191    mark_ms: f64,
192    mark_frozen_ms: f64,
193    frames: u64,
194    sources: Sources,
195}
196
197impl Pace<WallClock> {
198    pub fn new() -> Self {
199        let get = |key: &str| std::env::var(key).ok();
200        let cap = if uncapped(&get) { None } else { Some(CAP_FPS) };
201        let mut pace = Self::with_clock(WallClock::new(), cap);
202        pace.wall_ms = turn_wall_ms(&get);
203        pace
204    }
205}
206
207impl Default for Pace<WallClock> {
208    fn default() -> Self {
209        Self::new()
210    }
211}
212
213impl<C: FrameClock> Pace<C> {
214    pub fn with_clock(mut clock: C, cap_fps: Option<f64>) -> Self {
215        trace::arm();
216        let now = clock.now_ms();
217        let frozen = clock.frozen_ms();
218        Self {
219            clock,
220            wall_ms: TURN_WALL_MS,
221            tally: Tally::none(),
222            period_ms: cap_fps
223                .filter(|fps| fps.is_finite() && *fps > 0.0)
224                .map(|fps| 1000.0 / fps),
225            boundary_ms: now,
226            pending_ms: 0.0,
227            longest_ms: 0.0,
228            last_turn_ms: now,
229            mark_ms: now,
230            mark_frozen_ms: frozen,
231            frames: 0,
232            sources: Sources::default(),
233        }
234    }
235
236    pub fn with_tally(mut self, tally: Tally) -> Self {
237        self.tally = tally;
238        self
239    }
240
241    pub fn recording(mut self, sink: Box<dyn Sink>, planned: u64) -> Self {
242        let now = self.clock.now_ms();
243        self.tally = Tally::with_sink(sink, planned, now);
244        self
245    }
246
247    #[track_caller]
248    pub fn frame<R: FrameGpu>(&mut self, work: impl FnOnce() -> R) -> R {
249        let frozen_before = self.clock.frozen_ms();
250        let before = self.clock.now_ms();
251        let result = work();
252        let after = self.clock.now_ms();
253        let work_ms = unfrozen_ms(after - before, frozen_before, self.clock.frozen_ms());
254        self.after_gpu(work_ms, result.gpu_ms());
255        result
256    }
257
258    #[track_caller]
259    pub fn after(&mut self, frame_ms: f64) {
260        self.after_gpu(frame_ms, None);
261    }
262
263    #[track_caller]
264    pub fn after_gpu(&mut self, frame_ms: f64, gpu_ms: Option<f64>) {
265        let site = std::panic::Location::caller();
266        let since_mark = unfrozen_ms(
267            self.clock.now_ms() - self.mark_ms,
268            self.mark_frozen_ms,
269            self.clock.frozen_ms(),
270        );
271        let first = self.frames == 0;
272        self.frames += 1;
273        let gpu_ms = gpu_ms.filter(|ms| ms.is_finite() && *ms > 0.0);
274        let work_ms = match gpu_ms {
275            Some(ms) => ms,
276            None if first => 0.0,
277            None => frame_ms.min(since_mark),
278        };
279        self.sources.add(match gpu_ms {
280            Some(_) => Source::Gpu,
281            None if first => Source::Untimed,
282            None => Source::Wall,
283        });
284        if work_ms.is_finite() && work_ms > 0.0 {
285            self.pending_ms += work_ms;
286            self.longest_ms = self.longest_ms.max(work_ms);
287        }
288        self.tally.frame();
289        let now = self.clock.now_ms();
290        if self.pending_ms >= TURN_AFTER_MS || now - self.last_turn_ms >= self.wall_ms {
291            let report = TurnReport {
292                work_ms: self.pending_ms,
293                longest_ms: self.longest_ms,
294            };
295            trace::record(
296                "pace",
297                site,
298                report.work_ms,
299                report.longest_ms,
300                self.sources,
301            );
302            self.sources = Sources::default();
303            self.pending_ms = 0.0;
304            self.longest_ms = 0.0;
305            self.clock.turn(report);
306            trace::turned();
307            self.last_turn_ms = self.clock.now_ms();
308            self.tally.report(self.last_turn_ms);
309        }
310        if let Some(period) = self.period_ms {
311            let now = self.clock.now_ms();
312            let boundary = self.boundary_ms + period;
313            let wait = sleep_to(boundary, now);
314            if wait > 0.0 {
315                self.clock.sleep_ms(wait);
316                self.boundary_ms = boundary;
317            } else {
318                self.boundary_ms = now;
319            }
320        }
321        self.mark_ms = self.clock.now_ms();
322        self.mark_frozen_ms = self.clock.frozen_ms();
323    }
324}
325
326#[derive(Clone, Copy, Debug, PartialEq)]
327pub struct Frames {
328    pub p50: f64,
329    pub p95: f64,
330    pub p99: f64,
331    pub max: f64,
332    pub fps: f64,
333}
334
335impl Frames {
336    pub fn of(sorted_ms: &[f64]) -> Self {
337        let at =
338            |fraction: f64| sorted_ms[((sorted_ms.len() - 1) as f64 * fraction).round() as usize];
339        let p50 = at(0.50);
340        Self {
341            p50,
342            p95: at(0.95),
343            p99: at(0.99),
344            max: sorted_ms[sorted_ms.len() - 1],
345            fps: 1000.0 / p50,
346        }
347    }
348}
349
350#[cfg(test)]
351mod tests;