Skip to main content

alpha2_render/
submission_clock.rs

1//! How long each recent renderer submission kept the GPU busy.
2//!
3//! Windows resets a GPU whose submission exceeds its timeout, and a reset device's DX12 fence
4//! reads complete, so the loss looks like dropped work. The submissions' run times tell the two
5//! apart. Each submission's GPU time runs from the later of its submit and the previous
6//! submission's end to its own completion callback, so queueing is excluded. Callback latency
7//! (poll or event loop) is small beside [`alpha2_gpu::hung_threshold_ms`].
8
9use std::collections::VecDeque;
10use std::sync::Mutex;
11
12use web_time::Instant;
13
14/// Submissions kept; enough to cover the last frames' submissions.
15const KEPT: usize = 32;
16
17/// The frame a submission belongs to, as its caller knows it.
18#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
19pub struct FrameMark {
20    /// The frame's timeline time, in ticks.
21    pub time_ticks: i64,
22    /// The frame's index in the export, when it is one.
23    pub frame_index: Option<u64>,
24}
25
26/// The longest recent submission and what it rendered.
27#[derive(Clone, Debug, PartialEq)]
28pub struct SubmissionReport {
29    /// GPU time in milliseconds; for one still running, the time so far.
30    pub gpu_ms: f64,
31    /// The frame it rendered, when the caller marked one.
32    pub frame: Option<FrameMark>,
33    /// The effects it ran, as `id@version`.
34    pub effects: Vec<String>,
35    /// Predicted GPU time in milliseconds, when the frame was planned.
36    pub predicted_ms: Option<f64>,
37}
38
39#[derive(Debug)]
40struct Entry {
41    submitted_at: Instant,
42    done_at: Option<Instant>,
43    frame: Option<FrameMark>,
44    effects: Vec<String>,
45    predicted_ms: Option<f64>,
46}
47
48#[derive(Debug, Default)]
49struct ClockState {
50    /// Sequence number of `entries[0]`.
51    first: u64,
52    entries: VecDeque<Entry>,
53    frame: Option<FrameMark>,
54    effects: Vec<String>,
55}
56
57/// The renderer's recent submissions and when each finished.
58#[derive(Debug, Default)]
59pub(crate) struct SubmissionClock {
60    state: Mutex<ClockState>,
61}
62
63impl SubmissionClock {
64    fn state(&self) -> std::sync::MutexGuard<'_, ClockState> {
65        self.state
66            .lock()
67            .unwrap_or_else(std::sync::PoisonError::into_inner)
68    }
69
70    /// Marks the frame the next submissions render, until another is marked.
71    pub(crate) fn mark_frame(&self, frame: Option<FrameMark>) {
72        self.state().frame = frame;
73    }
74
75    /// Notes an effect the next submission runs.
76    pub(crate) fn note_effect(&self, effect: &crate::EffectId) {
77        let name = format!("{}@{}", effect.as_str(), effect.version());
78        let mut state = self.state();
79        if !state.effects.contains(&name) {
80            state.effects.push(name);
81        }
82    }
83
84    /// Records a submission made at `at` and returns its sequence number for [`Self::complete`].
85    pub(crate) fn record(&self, at: Instant, predicted_ms: Option<f64>) -> u64 {
86        let mut state = self.state();
87        let effects = std::mem::take(&mut state.effects);
88        let frame = state.frame;
89        state.entries.push_back(Entry {
90            submitted_at: at,
91            done_at: None,
92            frame,
93            effects,
94            predicted_ms,
95        });
96        if state.entries.len() > KEPT {
97            state.entries.pop_front();
98            state.first += 1;
99        }
100        state.first + u64::try_from(state.entries.len()).unwrap_or(u64::MAX) - 1
101    }
102
103    /// Records that submission `sequence` finished at `at`; the queue completes in order, so all
104    /// earlier ones have too.
105    pub(crate) fn complete(&self, sequence: u64, at: Instant) {
106        let mut state = self.state();
107        let Some(position) = sequence.checked_sub(state.first) else {
108            return;
109        };
110        let Ok(position) = usize::try_from(position) else {
111            return;
112        };
113        for entry in state.entries.iter_mut().take(position + 1) {
114            entry.done_at.get_or_insert(at);
115        }
116    }
117
118    /// Each kept submission's GPU time, oldest first: finished ones in full, the oldest unfinished
119    /// up to `now`, later unfinished ones not at all.
120    fn spans(state: &ClockState, now: Instant) -> Vec<(f64, &Entry)> {
121        let mut spans = Vec::new();
122        let mut previous_done: Option<Instant> = None;
123        for entry in &state.entries {
124            let start =
125                previous_done.map_or(entry.submitted_at, |done| done.max(entry.submitted_at));
126            let Some(done) = entry.done_at else {
127                spans.push((milliseconds(now.saturating_duration_since(start)), entry));
128                break;
129            };
130            spans.push((milliseconds(done.saturating_duration_since(start)), entry));
131            previous_done = Some(done);
132        }
133        spans
134    }
135
136    /// The kept submission with the longest GPU time, counting a running one up to `now`.
137    pub(crate) fn longest_at(&self, now: Instant) -> Option<SubmissionReport> {
138        let state = self.state();
139        Self::spans(&state, now)
140            .into_iter()
141            .max_by(|(a, _), (b, _)| a.total_cmp(b))
142            .map(|(gpu_ms, entry)| SubmissionReport {
143                gpu_ms,
144                frame: entry.frame,
145                effects: entry.effects.clone(),
146                predicted_ms: entry.predicted_ms,
147            })
148    }
149
150    /// Summed GPU time of `sequences` once all finished and are still kept. Late completions shift
151    /// time between consecutive submissions but not out of the sum, so a frame's total holds.
152    pub(crate) fn gpu_ms(&self, sequences: std::ops::Range<u64>) -> Option<f64> {
153        let state = self.state();
154        let first = state.first;
155        let spans = Self::spans(&state, Instant::now());
156        let mut total = 0.0;
157        let mut counted = 0;
158        for (position, (ms, entry)) in spans.into_iter().enumerate() {
159            let sequence = first + u64::try_from(position).unwrap_or(u64::MAX);
160            if sequences.contains(&sequence) {
161                entry.done_at?;
162                total += ms;
163                counted += 1;
164            }
165        }
166        (counted == sequences.end.saturating_sub(sequences.start)).then_some(total)
167    }
168
169    /// [`Self::longest_at`] now.
170    pub(crate) fn longest(&self) -> Option<SubmissionReport> {
171        self.longest_at(Instant::now())
172    }
173
174    /// The longest recent submission if it ran at least [`alpha2_gpu::hung_threshold_ms`], which
175    /// implicates this renderer's own work in a loss.
176    pub(crate) fn hung(&self) -> Option<SubmissionReport> {
177        self.longest()
178            .filter(|report| report.gpu_ms >= f64::from(alpha2_gpu::hung_threshold_ms()))
179    }
180}
181
182fn milliseconds(duration: std::time::Duration) -> f64 {
183    duration.as_secs_f64() * 1_000.0
184}
185
186#[cfg(test)]
187mod tests {
188    use std::time::Duration;
189
190    use super::*;
191
192    /// Submissions made at once run back to back; each starts where the previous ended.
193    #[test]
194    fn gpu_time_excludes_queueing_behind_earlier_submissions() {
195        let clock = SubmissionClock::default();
196        let start = Instant::now();
197        let at = |ms: u64| start + Duration::from_millis(ms);
198        let first = clock.record(at(0), None);
199        let second = clock.record(at(0), None);
200        let third = clock.record(at(0), None);
201        clock.complete(first, at(10));
202        clock.complete(second, at(400));
203        clock.complete(third, at(405));
204        let longest = clock.longest_at(at(1_000)).expect("submissions");
205        assert!((longest.gpu_ms - 390.0).abs() < 0.5, "{longest:?}");
206        // Submitted after the queue went idle, so it starts at its own submit.
207        let fourth = clock.record(at(2_000), None);
208        clock.complete(fourth, at(2_005));
209        assert!((clock.longest_at(at(3_000)).expect("kept").gpu_ms - 390.0).abs() < 0.5);
210        let _ = clock.record(at(3_000), None);
211        assert!((clock.longest_at(at(3_700)).expect("running").gpu_ms - 700.0).abs() < 0.5);
212    }
213
214    #[test]
215    fn a_completion_finishes_every_earlier_submission() {
216        let clock = SubmissionClock::default();
217        let start = Instant::now();
218        let first = clock.record(start, None);
219        let second = clock.record(start, None);
220        clock.complete(second, start + Duration::from_millis(8));
221        let _ = first;
222        assert_eq!(
223            clock
224                .longest_at(start + Duration::from_secs(5))
225                .map(|r| r.gpu_ms.round()),
226            Some(8.0)
227        );
228    }
229
230    #[test]
231    fn a_frames_gpu_time_survives_completions_seen_together() {
232        let clock = SubmissionClock::default();
233        let start = Instant::now();
234        let at = |ms: u64| start + Duration::from_millis(ms);
235        let earlier = clock.record(at(0), None);
236        clock.complete(earlier, at(5));
237        let first = clock.record(at(10), Some(100.0));
238        let second = clock.record(at(10), Some(100.0));
239        assert_eq!(clock.gpu_ms(first..second + 1), None, "not finished");
240        // Both seen at 210 ms: the first takes the whole span, and the sum is unchanged.
241        clock.complete(second, at(210));
242        let total = clock.gpu_ms(first..second + 1).expect("finished");
243        assert!((total - 200.0).abs() < 0.5, "{total}");
244        assert_eq!(
245            clock
246                .longest_at(at(300))
247                .and_then(|report| report.predicted_ms),
248            Some(100.0)
249        );
250    }
251
252    #[test]
253    fn the_clock_keeps_the_frame_and_effects_of_each_submission() {
254        let clock = SubmissionClock::default();
255        let start = Instant::now();
256        clock.mark_frame(Some(FrameMark {
257            time_ticks: 4_000,
258            frame_index: Some(1),
259        }));
260        clock.note_effect(&crate::EffectId::from_static("blur", 2));
261        let heavy = clock.record(start, None);
262        clock.complete(heavy, start + Duration::from_millis(30));
263        let light = clock.record(start + Duration::from_millis(40), None);
264        clock.complete(light, start + Duration::from_millis(41));
265        let report = clock
266            .longest_at(start + Duration::from_millis(50))
267            .expect("kept");
268        assert_eq!(report.effects, ["blur@2"]);
269        assert_eq!(report.frame.and_then(|frame| frame.frame_index), Some(1));
270    }
271}