Skip to main content

day_spec/
capture.rs

1// Copyright © The Daybrite Project
2// SPDX-License-Identifier: MPL-2.0
3
4//! Screenshot checkpoints. A revision identifies one ordered request, not a timer or
5//! an image hash. Completion of an older request cannot release a newer checkpoint.
6
7/// How a toolkit establishes freshness for a screenshot.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum Readiness {
10    /// The main loop must run before the checkpoint can complete.
11    Pending,
12    /// The toolkit completed its render checkpoint. The external capture still owns
13    /// synchronization with the compositor (and must not return a cached image).
14    Ready,
15    /// The toolkit's synchronous snapshot operation itself establishes freshness.
16    /// This does not promise that an external whole-screen capture is ready.
17    OnCapture,
18}
19
20/// UI-thread-owned state for an asynchronous capture checkpoint. Clones share state.
21/// Keep one per native window. Callbacks may outlive a timed-out request: `complete`
22/// deliberately ignores them after another revision has started.
23#[derive(Clone, Default)]
24pub struct Fence(std::rc::Rc<std::cell::RefCell<State>>);
25#[derive(Default)]
26struct State {
27    revision: Option<u32>,
28    result: Option<Result<(), String>>,
29}
30impl Fence {
31    /// Start once; repeated polls of the same revision must not request another frame.
32    pub fn begin(&self, revision: u32) -> bool {
33        let mut s = self.0.borrow_mut();
34        if s.revision == Some(revision) {
35            return false;
36        }
37        *s = State {
38            revision: Some(revision),
39            result: None,
40        };
41        true
42    }
43    pub fn complete(&self, revision: u32, result: Result<(), String>) {
44        let mut s = self.0.borrow_mut();
45        if s.revision == Some(revision) && s.result.is_none() {
46            s.result = Some(result);
47        }
48    }
49    pub fn poll(&self) -> Result<Readiness, String> {
50        match self.0.borrow().result.as_ref() {
51            None => Ok(Readiness::Pending),
52            Some(Ok(())) => Ok(Readiness::Ready),
53            Some(Err(e)) => Err(e.clone()),
54        }
55    }
56}
57
58/// Whether a host that presents frames of its own has every frame it owes on screen.
59///
60/// A renderer that paints outside the toolkit's frame — an offscreen engine presented through
61/// a canvas, its frames arriving after an asynchronous read-back and decode — is invisible to
62/// the toolkit's checkpoint: the checkpoint sees a committed frame, not that the host's latest
63/// frame was in it. A capture taken then shows the canvas as it was, or empty. Such a host
64/// registers one hold ([`register_hold`]); the dayscript capture steps wait, bounded by the
65/// step's timeout, while any hold answers `false`, before they request the checkpoint at all.
66/// Called on the UI thread, so the hold may read UI-thread state.
67pub type Hold = fn() -> bool;
68
69static HOLDS: std::sync::Mutex<Vec<Hold>> = std::sync::Mutex::new(Vec::new());
70
71/// Register a capture hold for the life of the process. Register once per host: every
72/// registration is consulted on every capture.
73pub fn register_hold(hold: Hold) {
74    HOLDS
75        .lock()
76        .unwrap_or_else(|poisoned| poisoned.into_inner())
77        .push(hold);
78}
79
80/// Whether every registered hold has settled. Holds run outside the registry's lock, so a
81/// hold may register another.
82pub fn holds_settled() -> bool {
83    let holds = HOLDS
84        .lock()
85        .unwrap_or_else(|poisoned| poisoned.into_inner())
86        .clone();
87    holds.iter().all(|hold| hold())
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    #[test]
94    fn checkpoints_do_not_rearm_or_accept_late_completions() {
95        let f = Fence::default();
96        assert!(f.begin(1));
97        assert!(!f.begin(1));
98        assert_eq!(f.poll(), Ok(Readiness::Pending));
99        assert!(f.begin(2));
100        f.clone().complete(1, Ok(()));
101        assert_eq!(f.poll(), Ok(Readiness::Pending));
102        f.complete(2, Ok(()));
103        assert_eq!(f.poll(), Ok(Readiness::Ready));
104        assert!(!f.begin(2));
105    }
106    #[test]
107    fn a_hold_defers_captures_until_it_settles() {
108        use std::sync::atomic::{AtomicBool, Ordering};
109        static SETTLED: AtomicBool = AtomicBool::new(false);
110        // Other tests in this process may have registered nothing; before any hold, settled.
111        assert!(holds_settled());
112        register_hold(|| SETTLED.load(Ordering::Relaxed));
113        assert!(!holds_settled());
114        SETTLED.store(true, Ordering::Relaxed);
115        assert!(holds_settled());
116    }
117    #[test]
118    fn errors_and_windows_are_independent() {
119        let a = Fence::default();
120        let b = Fence::default();
121        a.begin(7);
122        b.begin(7);
123        a.complete(7, Err("window closed".into()));
124        a.complete(7, Ok(()));
125        assert_eq!(a.poll(), Err("window closed".into()));
126        assert_eq!(b.poll(), Ok(Readiness::Pending));
127        a.begin(8);
128        a.complete(8, Ok(()));
129        assert_eq!(a.poll(), Ok(Readiness::Ready));
130    }
131}