Skip to main content

brep_app/
desktop_loop.rs

1//! The desktop event loop.
2//!
3//! `eframe::run_native` runs the app only under winit's `RedrawRequested`.
4//! On Wayland winit withholds that event while the surface's frame callback
5//! is outstanding (winit 0.30 `wayland/event_loop/mod.rs`, which skips a window
6//! whose callback is `Requested`, with no timeout), and a compositor sends no
7//! callbacks to a window it is not showing (another workspace, a hidden
8//! scratchpad, a covered window under some compositors). An automation command
9//! queued then waited until the user focused the window: the 2026-10-31
10//! desktop report's 29 s initial read. eframe's fallback for invisible windows
11//! does not apply, because Wayland reports neither visibility nor minimisation
12//! (`is_visible`/`is_minimized` return `None`). And once a repaint is
13//! outstanding with no pass run, egui stops calling its repaint callback, so a
14//! later submit did not even wake the loop.
15//!
16//! `run` owns the winit loop instead (`eframe::create_native`) and wraps
17//! eframe's handler in `DesktopLoop`. The automation queue's waker sends the
18//! loop its own event (never forwarded to eframe), and after every batch of
19//! events the loop runs `BrepApp::service_automation` on this, the app's
20//! thread, whenever the queue's head can be dispatched without a frame. Frames
21//! still drain the queue exactly as before; this path draws nothing.
22//!
23//! A command that needs more than app state (input egui must apply, a layout
24//! read, a capture) does not wait for a frame either: the loop runs an egui
25//! pass itself, on demand (`run_pass`). It is the same pass eframe runs
26//! (`raw_input_hook`, then `logic` and `ui` inside `Context::run_ui`), on the
27//! window's last known size and clock, and it is never presented: a capture
28//! asked for in it is drawn into an offscreen texture (`crate::offscreen`).
29//! Everything else the pass hands back goes where eframe would have put it:
30//! its textures to the shared renderer, its viewport and platform commands
31//! back onto the context for eframe's next real frame, and a capture's
32//! `Event::Screenshot` into the next pass's input, as eframe delivers one.
33//!
34//! The loop also keeps an undrawn window idle. eframe arms
35//! `ControlFlow::Poll` whenever it requests a redraw (`check_redraw_requests`)
36//! and disarms it only in the pass that redraw runs, so a redraw the
37//! compositor never delivers leaves the loop polling, a core spinning while
38//! idle. winit wakes the loop itself when a requested redraw is delivered, so
39//! after one polled iteration without a `RedrawRequested`, the loop waits.
40use crate::app::BrepApp;
41use eframe::UserEvent;
42use std::cell::RefCell;
43use std::rc::Rc;
44use std::sync::{Arc, Mutex};
45use std::time::{Duration, Instant};
46use winit::application::ApplicationHandler;
47use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
48
49/// Wakes the loop thread (the automation queue's waker).
50pub type Waker = Arc<dyn Fn() + Send + Sync>;
51
52/// The pass number the wake event carries. It is never a real pass, and the
53/// event is never forwarded to eframe, so egui's repaint bookkeeping cannot
54/// see or swallow it.
55const WAKE: u64 = u64::MAX;
56
57/// While work a frame-free dispatch started is still in flight (a background
58/// history run, a store write) and no frame comes, it is serviced at the frame
59/// cadence: the rate a drawn window pumps it. Background runs report
60/// completion only by being polled (`Documents::pump_all`). This paces
61/// completion of accepted work only; dispatch itself is event-driven.
62const SERVICE_PACE: Duration = Duration::from_millis(16);
63
64/// The app, shared on the loop thread between eframe (through [`Shell`]) and
65/// the loop's service step. The two never run at once: winit delivers events
66/// to one handler, sequentially, on this thread.
67#[derive(Clone, Default)]
68pub struct Slot(Rc<RefCell<Option<Installed>>>);
69
70#[derive(Clone)]
71struct Installed {
72    app: Rc<RefCell<BrepApp>>,
73    ctx: egui::Context,
74    /// The window's renderer (shared with eframe's painter): an on-demand
75    /// pass uploads its textures there and draws its captures with it.
76    render: Option<eframe::egui_wgpu::RenderState>,
77    pass: Rc<RefCell<PassInput>>,
78}
79
80/// What an on-demand pass needs to stand in for eframe's input: the clock
81/// eframe's passes run on, the window's size and scale, and the events a
82/// capture delivers to the pass after it.
83struct PassInput {
84    /// `(egui time, when)` of the last real pass, to continue its clock.
85    clock: Option<(f64, Instant)>,
86    /// When the app was installed: the clock's origin before any real pass.
87    started: Instant,
88    /// The window's inner size in physical pixels, from `Resized`.
89    size: Option<winit::dpi::PhysicalSize<u32>>,
90    /// The window's scale factor, from `ScaleFactorChanged`.
91    scale: Option<f64>,
92    /// Events for the next pass, real or on demand (`Event::Screenshot`).
93    events: Vec<egui::Event>,
94}
95
96impl PassInput {
97    fn new() -> Self {
98        PassInput { clock: None, started: Instant::now(), size: None, scale: None, events: Vec::new() }
99    }
100
101    fn now(&self) -> f64 {
102        match self.clock {
103            Some((time, at)) => time + at.elapsed().as_secs_f64(),
104            None => self.started.elapsed().as_secs_f64(),
105        }
106    }
107}
108
109impl Slot {
110    /// Install the app from the eframe creation closure; returns eframe's
111    /// handle to it.
112    pub fn install(&self, app: BrepApp, ctx: &egui::Context, render: Option<eframe::egui_wgpu::RenderState>) -> Shell {
113        let app = Rc::new(RefCell::new(app));
114        let pass = Rc::new(RefCell::new(PassInput::new()));
115        *self.0.borrow_mut() = Some(Installed { app: app.clone(), ctx: ctx.clone(), render, pass: pass.clone() });
116        Shell(app, pass)
117    }
118
119    fn get(&self) -> Option<Installed> {
120        self.0.borrow().clone()
121    }
122}
123
124/// eframe's view of the shared app: every [`eframe::App`] method forwards.
125pub struct Shell(Rc<RefCell<BrepApp>>, Rc<RefCell<PassInput>>);
126
127impl eframe::App for Shell {
128    fn logic(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
129        eframe::App::logic(&mut *self.0.borrow_mut(), ctx, frame)
130    }
131    fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
132        eframe::App::ui(&mut *self.0.borrow_mut(), ui, frame)
133    }
134    fn save(&mut self, storage: &mut dyn eframe::Storage) {
135        eframe::App::save(&mut *self.0.borrow_mut(), storage)
136    }
137    fn on_exit(&mut self) {
138        eframe::App::on_exit(&mut *self.0.borrow_mut())
139    }
140    fn auto_save_interval(&self) -> Duration {
141        eframe::App::auto_save_interval(&*self.0.borrow())
142    }
143    fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] {
144        eframe::App::clear_color(&*self.0.borrow(), visuals)
145    }
146    fn persist_egui_memory(&self) -> bool {
147        eframe::App::persist_egui_memory(&*self.0.borrow())
148    }
149    fn raw_input_hook(&mut self, ctx: &egui::Context, raw_input: &mut egui::RawInput) {
150        {
151            // A real pass: an on-demand one continues its clock, and the
152            // events an on-demand capture left for "the next pass" are this one's.
153            let mut pass = self.1.borrow_mut();
154            if let Some(time) = raw_input.time {
155                pass.clock = Some((time, Instant::now()));
156            }
157            raw_input.events.append(&mut pass.events);
158        }
159        eframe::App::raw_input_hook(&mut *self.0.borrow_mut(), ctx, raw_input)
160    }
161}
162
163/// How many passes the loop has run on demand (never presented).
164static ON_DEMAND_PASSES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
165
166/// How many passes the loop has run on demand in this process.
167pub fn on_demand_passes() -> u64 {
168    ON_DEMAND_PASSES.load(std::sync::atomic::Ordering::SeqCst)
169}
170
171/// On-demand passes run at most at the frame cadence. The egui clock is wall
172/// time in them as in a drawn frame, so what counts time in frames (a click's
173/// timing, a dwell, `wait_frames`) sees what a drawn window would; and the
174/// loop goes back to the window's own events between two of them.
175const PASS_PACE: Duration = SERVICE_PACE;
176
177/// The surface an on-demand pass lays out on before the window has reported
178/// any size (in points; winit's default window size, which eframe keeps).
179const FALLBACK_SIZE: egui::Vec2 = egui::vec2(800.0, 600.0);
180
181/// Run one egui pass of the app now, as eframe's `run_ui_and_paint` would,
182/// but without presenting it. See the module doc.
183#[cfg(feature = "automation")]
184fn run_pass(app: &mut BrepApp, installed: &Installed) {
185    let ctx = &installed.ctx;
186    let mut raw = {
187        let mut pass = installed.pass.borrow_mut();
188        let mut raw = ctx.input(|i| i.raw.clone());
189        // The last pass's input stands for the window (its size, scale,
190        // focus, texture limit); its events are spent.
191        raw.events = std::mem::take(&mut pass.events);
192        raw.hovered_files.clear();
193        raw.dropped_files.clear();
194        raw.time = Some(pass.now());
195        let root = raw.viewports.entry(egui::ViewportId::ROOT).or_default();
196        root.events.clear();
197        if let Some(scale) = pass.scale {
198            root.native_pixels_per_point = Some(scale as f32);
199        }
200        // egui-winit's rule: points = physical pixels / (native scale x zoom).
201        let pixels_per_point = root.native_pixels_per_point.unwrap_or(1.0) * ctx.zoom_factor();
202        if let Some(size) = pass.size.filter(|s| s.width > 0 && s.height > 0) {
203            raw.screen_rect = Some(egui::Rect::from_min_size(
204                egui::Pos2::ZERO,
205                egui::vec2(size.width as f32, size.height as f32) / pixels_per_point,
206            ));
207        }
208        if raw.screen_rect.is_none() {
209            // No pass and no `Resized` yet: the size eframe opened the window
210            // at, rather than egui's unbounded default.
211            raw.screen_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, FALLBACK_SIZE));
212        }
213        if raw.max_texture_side.is_none() {
214            raw.max_texture_side = installed.render.as_ref().map(|r| r.device.limits().max_texture_dimension_2d as usize);
215        }
216        raw
217    };
218    // Counted as it begins: the pass answers commands from inside it.
219    ON_DEMAND_PASSES.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
220    eframe::App::raw_input_hook(app, ctx, &mut raw);
221    let mut frame = eframe::Frame::_new_kittest();
222    let output = ctx.run_ui(raw, |ui| {
223        eframe::App::logic(app, ui.ctx(), &mut frame);
224        eframe::App::ui(app, ui, &mut frame);
225    });
226    let egui::FullOutput { platform_output, textures_delta, shapes, pixels_per_point, mut viewport_output } = output;
227    if let Some(render) = &installed.render {
228        crate::offscreen::apply_texture_sets(render, &textures_delta);
229    }
230    // Viewport commands: a capture is drawn here; everything else goes back
231    // onto the context for eframe's next real frame to carry out.
232    let mut captures = Vec::new();
233    for (id, viewport) in viewport_output.iter_mut() {
234        for command in std::mem::take(&mut viewport.commands) {
235            match command {
236                egui::ViewportCommand::Screenshot(user_data) if *id == egui::ViewportId::ROOT => captures.push(user_data),
237                other => ctx.send_viewport_cmd_to(*id, other),
238            }
239        }
240    }
241    if !captures.is_empty() {
242        let size = ctx.content_rect().size() * pixels_per_point;
243        let size_px = [size.x.round() as u32, size.y.round() as u32];
244        let clear = eframe::App::clear_color(app, &ctx.global_style().visuals);
245        let image = match &installed.render {
246            Some(render) => crate::offscreen::capture(render, ctx, shapes, pixels_per_point, size_px, clear),
247            None => Err("offscreen capture: the window has no wgpu render state".to_string()),
248        };
249        let mut pass = installed.pass.borrow_mut();
250        match image {
251            Ok(image) => {
252                let image = std::sync::Arc::new(image);
253                for user_data in captures {
254                    pass.events.push(egui::Event::Screenshot { viewport_id: egui::ViewportId::ROOT, user_data, image: image.clone() });
255                }
256            }
257            Err(error) => {
258                log::error!("{error}");
259                drop(pass);
260                app.automation().fail_captures(&error);
261            }
262        }
263    }
264    if let Some(render) = &installed.render {
265        crate::offscreen::apply_texture_frees(render, &textures_delta);
266    }
267    // What the user would have been given (a copy, an opened link) still
268    // happens, on eframe's next real frame.
269    if !platform_output.commands.is_empty() {
270        ctx.output_mut(|o| o.commands.extend(platform_output.commands));
271    }
272    // A window that IS drawn shows the pass's effect on its next frame.
273    ctx.request_repaint();
274}
275
276/// A waker that sends the loop its wake event. Safe from any thread; a closed
277/// loop ignores it.
278pub fn waker(proxy: EventLoopProxy<UserEvent>) -> Waker {
279    let proxy = Mutex::new(proxy);
280    Arc::new(move || {
281        let event = UserEvent::RequestRepaint { viewport_id: egui::ViewportId::ROOT, when: Instant::now(), cumulative_pass_nr: WAKE };
282        let _ = proxy.lock().unwrap_or_else(|p| p.into_inner()).send_event(event);
283    })
284}
285
286fn is_wake(event: &UserEvent) -> bool {
287    matches!(event, UserEvent::RequestRepaint { cumulative_pass_nr: WAKE, .. })
288}
289
290/// eframe's winit handler plus the frame-free service step.
291pub struct DesktopLoop<H> {
292    inner: H,
293    slot: Slot,
294    /// Work the last service step started is still in flight.
295    busy: bool,
296    /// The `WaitUntil` deadline the service step armed to pace that work.
297    paced: Option<Instant>,
298    /// The last iteration ended polling (eframe requested a redraw).
299    polled: bool,
300    /// A `RedrawRequested` arrived since the last `about_to_wait`.
301    redrawn: bool,
302    /// When the loop last ran a pass on demand ([`PASS_PACE`]).
303    last_pass: Option<Instant>,
304}
305
306impl<H: ApplicationHandler<UserEvent>> DesktopLoop<H> {
307    pub fn new(inner: H, slot: Slot) -> Self {
308        DesktopLoop { inner, slot, busy: false, paced: None, polled: false, redrawn: false, last_pass: None }
309    }
310
311    fn with_pass(&self, f: impl FnOnce(&mut PassInput)) {
312        if let Some(installed) = self.slot.get() {
313            f(&mut installed.pass.borrow_mut());
314        }
315    }
316
317    /// End eframe's poll for a redraw that did not come within one polled
318    /// iteration (a hidden Wayland window gets none). Stock behaviour while
319    /// redraws are delivered: each one disarms the poll in its own pass.
320    fn unstick_poll(&mut self, event_loop: &ActiveEventLoop) {
321        if event_loop.control_flow() == ControlFlow::Poll {
322            if self.polled && !self.redrawn {
323                event_loop.set_control_flow(ControlFlow::Wait);
324                self.polled = false;
325            } else {
326                self.polled = true;
327            }
328        } else {
329            self.polled = false;
330        }
331        self.redrawn = false;
332    }
333
334    /// One frame-free service step, when the queue's head needs no frame or
335    /// work it started is still in flight.
336    fn service(&mut self, event_loop: &ActiveEventLoop) {
337        #[cfg(feature = "automation")]
338        self.step();
339        #[cfg(not(feature = "automation"))]
340        let _ = &self.slot;
341        // The deadline this step armed last time, if the loop still holds it
342        // (eframe replaces it with its own when it schedules a repaint).
343        let ours = self.paced.take().filter(|at| event_loop.control_flow() == ControlFlow::WaitUntil(*at));
344        if self.busy {
345            let next = Instant::now() + SERVICE_PACE;
346            match event_loop.control_flow() {
347                ControlFlow::Poll => {}
348                // eframe's own earlier wake stands; ours is renewed.
349                ControlFlow::WaitUntil(at) if at <= next && ours.is_none() => {}
350                _ => {
351                    event_loop.set_control_flow(ControlFlow::WaitUntil(next));
352                    self.paced = Some(next);
353                }
354            }
355        } else if ours.is_some() {
356            // Work done: a deadline left in the past would wake the loop at
357            // once, every iteration.
358            event_loop.set_control_flow(ControlFlow::Wait);
359        }
360    }
361
362    /// Dispatch what needs no pass, and run the pass the queue's head needs
363    /// ([`PASS_PACE`] after the last one; until then the step stays busy, so
364    /// the loop comes back for it).
365    #[cfg(feature = "automation")]
366    fn step(&mut self) {
367        let Some(installed) = self.slot.get() else { return };
368        // Only eframe holds this borrow, and never across a loop callback.
369        let Ok(mut app) = installed.app.try_borrow_mut() else { return };
370        let queue = app.automation().clone();
371        let owed = loop {
372            if self.busy || queue.has_frameless_work() {
373                self.busy = app.service_automation(&installed.ctx);
374            }
375            let events = !installed.pass.borrow().events.is_empty();
376            // A measured spin advances one step per pass.
377            let owed = queue.needs_pass() || events || app.spinning();
378            if !owed || self.last_pass.is_some_and(|at| at.elapsed() < PASS_PACE) {
379                break owed;
380            }
381            run_pass(&mut app, &installed);
382            self.last_pass = Some(Instant::now());
383        };
384        self.busy |= owed;
385    }
386}
387
388impl<H: ApplicationHandler<UserEvent>> ApplicationHandler<UserEvent> for DesktopLoop<H> {
389    fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: winit::event::StartCause) {
390        self.inner.new_events(event_loop, cause);
391    }
392
393    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
394        self.inner.resumed(event_loop);
395    }
396
397    fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
398        // The wake is ours alone; `about_to_wait`, which always follows the
399        // batch it arrived in, services the queue.
400        if !is_wake(&event) {
401            self.inner.user_event(event_loop, event);
402        }
403    }
404
405    fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: winit::window::WindowId, event: winit::event::WindowEvent) {
406        self.redrawn |= matches!(event, winit::event::WindowEvent::RedrawRequested);
407        // The window's size and scale, for an on-demand pass (eframe keeps
408        // its own copy where the loop cannot read it).
409        let resized = match &event {
410            winit::event::WindowEvent::Resized(size) => {
411                self.with_pass(|pass| pass.size = Some(*size));
412                true
413            }
414            winit::event::WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
415                let scale = *scale_factor;
416                self.with_pass(|pass| pass.scale = Some(scale));
417                true
418            }
419            _ => false,
420        };
421        self.inner.window_event(event_loop, window_id, event);
422        #[cfg(feature = "automation")]
423        if resized {
424            if let Some(installed) = self.slot.get() {
425                if let Ok(app) = installed.app.try_borrow() {
426                    app.automation().mark_layout_dirty();
427                }
428            }
429        }
430        #[cfg(not(feature = "automation"))]
431        let _ = resized;
432    }
433
434    fn device_event(&mut self, event_loop: &ActiveEventLoop, device_id: winit::event::DeviceId, event: winit::event::DeviceEvent) {
435        self.inner.device_event(event_loop, device_id, event);
436    }
437
438    /// After every batch of events, including the batch a wake arrived in
439    /// and the one in which eframe created the app: a command queued before
440    /// the app existed, or before the waker was installed, is found here.
441    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
442        self.inner.about_to_wait(event_loop);
443        self.unstick_poll(event_loop);
444        if !event_loop.exiting() {
445            self.service(event_loop);
446        }
447    }
448
449    fn suspended(&mut self, event_loop: &ActiveEventLoop) {
450        self.inner.suspended(event_loop);
451    }
452
453    fn exiting(&mut self, event_loop: &ActiveEventLoop) {
454        self.inner.exiting(event_loop);
455    }
456
457    fn memory_warning(&mut self, event_loop: &ActiveEventLoop) {
458        self.inner.memory_warning(event_loop);
459    }
460}
461
462/// What the creation closure is given: eframe's context and the waker to
463/// install on the app's automation queue before anything can submit to it.
464pub type Create<'a> = Box<dyn 'a + FnOnce(&eframe::CreationContext<'_>, Waker) -> Result<BrepApp, String>>;
465
466/// Run the app on a new event loop.
467pub fn run(app_name: &str, options: eframe::NativeOptions, create: Create<'_>) -> Result<(), String> {
468    let event_loop = EventLoop::<UserEvent>::with_user_event().build().map_err(|e| format!("event loop: {e}"))?;
469    run_on(event_loop, app_name, options, create, |eframe| eframe)
470}
471
472/// Run the app on `event_loop`. `wrap` sits between [`DesktopLoop`] and
473/// eframe's handler: the identity in production; a test interposes there to
474/// play a compositor that withholds frames.
475pub fn run_on<'a, H: ApplicationHandler<UserEvent>>(
476    event_loop: EventLoop<UserEvent>,
477    app_name: &str,
478    options: eframe::NativeOptions,
479    create: Create<'a>,
480    wrap: impl FnOnce(eframe::EframeWinitApplication<'a>) -> H,
481) -> Result<(), String> {
482    let slot = Slot::default();
483    let failure: Rc<RefCell<Option<String>>> = Rc::default();
484    let waker = waker(event_loop.create_proxy());
485    let (installer, failed) = (slot.clone(), failure.clone());
486    // A history run (or any runner reply) landing wakes the loop to collect
487    // it, rather than waiting for the service step's next timed check.
488    brep_render::runner::set_reply_waker(Some(waker.clone()));
489    let creator: eframe::AppCreator<'a> = Box::new(move |cc| match create(cc, waker) {
490        Ok(app) => Ok(Box::new(installer.install(app, &cc.egui_ctx, cc.wgpu_render_state.clone())) as Box<dyn eframe::App>),
491        Err(error) => {
492            *failed.borrow_mut() = Some(error.clone());
493            Err(error.into())
494        }
495    });
496    let eframe = eframe::create_native(app_name, options, creator, &event_loop);
497    let mut handler = DesktopLoop::new(wrap(eframe), slot);
498    event_loop.run_app(&mut handler).map_err(|e| format!("event loop: {e}"))?;
499    let failed = failure.borrow_mut().take();
500    match failed {
501        Some(error) => Err(error),
502        None => Ok(()),
503    }
504}