Skip to main content

ashlar_app/
driver.rs

1//! A headless event loop for tests and benches: runs a [`Runtime`] on an egui context without a
2//! window, the way eframe does, on a virtual clock. A frame runs only when something asked for
3//! one (input, `request_repaint`, a repaint timer), so tests can prove that an idle app draws
4//! nothing.
5//!
6//! There is no GPU: `ui.screenshot` gets a blank image of the screen size (the PNG plumbing is
7//! real, the pixels are not).
8
9use std::collections::VecDeque;
10use std::sync::{Arc, Mutex, PoisonError};
11use std::time::Duration;
12
13use ashlar_control::{Reply, Request};
14use egui::{Event, Rect, ViewportCommand, ViewportId, pos2, vec2};
15use serde_json::Value;
16
17use crate::runtime::Runtime;
18use crate::session::Document;
19
20/// Repaint requests made outside a pass (another thread: a job, the control server).
21#[derive(Default)]
22struct Wakes {
23    /// (delay, pass number when requested)
24    pending: Vec<(Duration, u64)>,
25}
26
27/// Drives a [`Runtime`] headless. See the module docs.
28pub struct Driver<D: Document> {
29    pub runtime: Runtime<D>,
30    ctx: egui::Context,
31    time: f64,
32    wakes: Arc<Mutex<Wakes>>,
33    /// Virtual time of the next frame egui asked for.
34    next: Option<f64>,
35    input: VecDeque<Event>,
36    screen: Rect,
37    frames: u64,
38    closed: bool,
39    close_requested: bool,
40    /// Deliver `ViewportCommand::Screenshot` captures (off: a window that presents no frame).
41    screenshots: bool,
42    /// The window is minimized: frames run only [`Runtime::logic`], as eframe does.
43    hidden: bool,
44}
45
46impl<D: Document> Driver<D> {
47    /// A 1280×800 point screen at time 0.
48    pub fn new(runtime: Runtime<D>) -> Self {
49        let ctx = egui::Context::default();
50        let wakes: Arc<Mutex<Wakes>> = Arc::default();
51        let w = Arc::clone(&wakes);
52        ctx.set_request_repaint_callback(move |info| {
53            if info.viewport_id == ViewportId::ROOT {
54                w.lock().unwrap_or_else(PoisonError::into_inner).pending.push((info.delay, info.current_cumulative_pass_nr));
55            }
56        });
57        Driver {
58            runtime,
59            ctx,
60            time: 0.0,
61            wakes,
62            next: Some(0.0),
63            input: VecDeque::new(),
64            screen: Rect::from_min_size(pos2(0.0, 0.0), vec2(1280.0, 800.0)),
65            frames: 0,
66            closed: false,
67            close_requested: false,
68            screenshots: true,
69            hidden: false,
70        }
71    }
72
73    /// Stop driving; the runtime is handed back (e.g. to [`Runtime::abandon`] it).
74    pub fn into_runtime(self) -> Runtime<D> {
75        self.runtime
76    }
77
78    pub fn ctx(&self) -> &egui::Context {
79        &self.ctx
80    }
81
82    pub fn set_screen(&mut self, width: f32, height: f32) {
83        self.screen = Rect::from_min_size(pos2(0.0, 0.0), vec2(width, height));
84        self.next = Some(self.time);
85    }
86
87    /// Virtual seconds since the start.
88    pub fn time(&self) -> f64 {
89        self.time
90    }
91
92    /// Frames run so far.
93    pub fn frames(&self) -> u64 {
94        self.frames
95    }
96
97    /// The app closed its window.
98    pub fn is_closed(&self) -> bool {
99        self.closed
100    }
101
102    /// Queue raw input for the next frame (it also wakes the loop).
103    pub fn push_event(&mut self, e: Event) {
104        self.input.push_back(e);
105    }
106
107    /// Whether screenshots are delivered (on by default). Off, the loop behaves like a window
108    /// that is covered or on a locked screen: capture requests are dropped.
109    pub fn set_screenshots(&mut self, deliver: bool) {
110        self.screenshots = deliver;
111    }
112
113    /// Minimize the window (or show it again). Hidden, a frame runs [`Runtime::logic`] with no
114    /// egui pass, the way eframe treats a minimized or covered window, then the runtime's
115    /// [`Runtime::hidden_pass`] for pending debugger work; other input waits for the next shown
116    /// frame. A raise from the app (`Focus`, `Minimized(false)`, `Visible(true)`) shows it again.
117    pub fn set_hidden(&mut self, hidden: bool) {
118        self.hidden = hidden;
119        self.next = Some(self.time);
120    }
121
122    /// Whether the window is hidden ([`Driver::set_hidden`]).
123    pub fn is_hidden(&self) -> bool {
124        self.hidden
125    }
126
127    /// Ask to close the window, as the window's close button does.
128    pub fn request_close(&mut self) {
129        self.close_requested = true;
130    }
131
132    /// Run one frame now, whether or not one was asked for.
133    pub fn frame(&mut self) -> egui::FullOutput {
134        if self.hidden {
135            self.logic_only();
136            return egui::FullOutput::default();
137        }
138        let mut raw = egui::RawInput { screen_rect: Some(self.screen), time: Some(self.time), focused: true, ..Default::default() };
139        raw.events.extend(self.input.drain(..));
140        if std::mem::take(&mut self.close_requested) {
141            raw.viewports.entry(ViewportId::ROOT).or_default().events.push(egui::ViewportEvent::Close);
142        }
143        let ctx = self.ctx.clone();
144        self.runtime.raw_input_hook(&ctx, &mut raw);
145        let rt = &mut self.runtime;
146        let mut out = ctx.run_ui(raw, |ui| {
147            rt.logic(ui.ctx());
148            rt.ui(ui);
149        });
150        out.textures_delta.clear();
151        self.frames = self.frames.saturating_add(1);
152        let done = self.ctx.cumulative_pass_nr();
153        self.next = None;
154        if let Some(vp) = out.viewport_output.get(&ViewportId::ROOT) {
155            if vp.repaint_delay < Duration::MAX {
156                self.next = Some(self.time + vp.repaint_delay.as_secs_f64());
157            }
158            for c in &vp.commands {
159                match c {
160                    ViewportCommand::Screenshot(_) if !self.screenshots => {}
161                    ViewportCommand::Screenshot(user_data) => {
162                        let [w, h] = [self.screen.width() as usize, self.screen.height() as usize];
163                        let image = Arc::new(egui::ColorImage::new([w, h], vec![egui::Color32::from_gray(30); w * h]));
164                        self.input.push_back(Event::Screenshot { viewport_id: ViewportId::ROOT, user_data: user_data.clone(), image });
165                    }
166                    ViewportCommand::Close => self.closed = true,
167                    _ => {}
168                }
169            }
170        }
171        // requests made during the passes just run are in `repaint_delay`; keep later ones
172        self.wakes.lock().unwrap_or_else(PoisonError::into_inner).pending.retain(|(_, nr)| *nr >= done);
173        out
174    }
175
176    /// A frame of a hidden window: `logic` only, and the viewport commands it gave.
177    fn logic_only(&mut self) {
178        let mut raw = egui::RawInput { screen_rect: Some(self.screen), time: Some(self.time), focused: false, ..Default::default() };
179        raw.viewports.entry(ViewportId::ROOT).or_default().minimized = Some(true);
180        // screenshots the last pass asked for arrive with this one (a hidden window has none to
181        // present; this stands in for the debugger's off-screen renderer)
182        let (shots, rest): (Vec<Event>, Vec<Event>) = self.input.drain(..).partition(|e| matches!(e, Event::Screenshot { .. }));
183        self.input.extend(rest);
184        // wakes asked for from here on (also by a hidden pass, which has no repaint delay to
185        // report) stay pending
186        let done = self.ctx.cumulative_pass_nr();
187        let (rt, time) = (&mut self.runtime, self.time);
188        let out = self.ctx.run_logic(&raw, |ctx| {
189            rt.logic(ctx);
190            // nothing to upload textures to
191            if let Some(mut out) = rt.hidden_pass(ctx, Some(time), shots) {
192                out.textures_delta.clear();
193            }
194        });
195        self.frames = self.frames.saturating_add(1);
196        self.next = None;
197        for c in out.viewport_commands.get(&ViewportId::ROOT).into_iter().flatten() {
198            match c {
199                ViewportCommand::Screenshot(user_data) if self.screenshots => {
200                    let [w, h] = [self.screen.width() as usize, self.screen.height() as usize];
201                    let image = Arc::new(egui::ColorImage::new([w, h], vec![egui::Color32::from_gray(30); w * h]));
202                    self.input.push_back(Event::Screenshot { viewport_id: ViewportId::ROOT, user_data: user_data.clone(), image });
203                    self.next = Some(self.time);
204                }
205                ViewportCommand::Focus | ViewportCommand::Minimized(false) | ViewportCommand::Visible(true) => {
206                    self.hidden = false;
207                    self.next = Some(self.time);
208                }
209                ViewportCommand::Close => self.closed = true,
210                _ => {}
211            }
212        }
213        self.wakes.lock().unwrap_or_else(PoisonError::into_inner).pending.retain(|(_, nr)| *nr >= done);
214    }
215
216    /// When the next frame is due, if one is.
217    fn due(&self) -> Option<f64> {
218        let wake =
219            self.wakes.lock().unwrap_or_else(PoisonError::into_inner).pending.iter().map(|(d, _)| self.time + d.as_secs_f64()).reduce(f64::min);
220        // a hidden window keeps its input for the next shown frame
221        let input = ((!self.input.is_empty() && !self.hidden) || self.close_requested).then_some(self.time);
222        [self.next, wake, input].into_iter().flatten().reduce(f64::min)
223    }
224
225    /// Let `secs` of virtual time pass, running every frame that is asked for (at most one per
226    /// `1/120` s, like a display would). Returns the frames run.
227    pub fn advance(&mut self, secs: f64) -> u64 {
228        let end = self.time + secs.max(0.0);
229        let start = self.frames;
230        let min_dt = 1.0 / 120.0;
231        let mut last_frame = f64::NEG_INFINITY;
232        while !self.closed {
233            let Some(due) = self.due() else { break };
234            let at = due.max(last_frame + min_dt).max(self.time);
235            if at > end {
236                break;
237            }
238            self.time = at;
239            self.frame();
240            last_frame = at;
241        }
242        self.time = self.time.max(end);
243        self.frames - start
244    }
245
246    /// Run frames until none is asked for (at most `max`); returns how many ran. Timers further
247    /// than `horizon` seconds away do not count as pending.
248    pub fn settle(&mut self, max: u64) -> u64 {
249        let start = self.frames;
250        let horizon = 0.25;
251        while self.frames - start < max && !self.closed {
252            match self.due() {
253                Some(t) if t <= self.time + horizon => {
254                    self.time = t.max(self.time) + 1.0 / 120.0;
255                    self.frame();
256                }
257                _ => break,
258            }
259        }
260        self.frames - start
261    }
262
263    /// Send a control request (as the control port would) and run frames until it is answered:
264    /// one frame for the request, then only the frames something asks for, on the virtual clock
265    /// (a request that needs frames nobody asks for is reported, not papered over by polling).
266    pub fn request(&mut self, method: &str, params: Value) -> Reply {
267        let (req, rx) = Request::new(method, params);
268        self.runtime.inject_request(req);
269        self.time += 1.0 / 120.0;
270        self.frame();
271        let limit = self.time + 600.0;
272        for _ in 0..10_000 {
273            if let Ok(r) = rx.try_recv() {
274                return r;
275            }
276            if self.closed {
277                break;
278            }
279            let Some(due) = self.due().filter(|d| *d <= limit) else { break };
280            self.time = due.max(self.time + 1.0 / 120.0);
281            self.frame();
282        }
283        rx.try_recv().unwrap_or_else(|_| Reply::err(format!("`{method}` was not answered, and no frame is pending")))
284    }
285
286    /// [`Driver::request`] expecting success.
287    pub fn call(&mut self, method: &str, params: Value) -> Result<Value, String> {
288        self.request(method, params).into_result().map_err(|(e, _)| e)
289    }
290}