Skip to main content

teksilo_platform/
wake.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! A window's wake target: the [`RedrawWaker`] any thread uses to have one
5//! window run a frame.
6//!
7//! Two routes, chosen by what the wake keeps current (see
8//! [`teksilo_canvas::wake`]):
9//!
10//! - **[`WakeKind::Draw`]** asks winit for a redraw. It is dropped while the
11//!   window is hidden, and one redraw is requested when it is shown again. A
12//!   request winit is already holding (a Wayland window waiting for a frame
13//!   callback) merges into it, which is right for a wake that only changes
14//!   pixels: the redraw winit delivers draws the latest state.
15//! - **[`WakeKind::Layout`]** keeps state current, so it must reach a window
16//!   that draws nothing. A redraw request cannot promise that: on Wayland,
17//!   while winit holds a redraw back, a further request does not even wake
18//!   the event loop (it sets a flag that is already set). It goes through the
19//!   *state route* teksilo-app installs, which posts one event per burst to
20//!   the event loop; without a route it falls back to a redraw request.
21//!
22//! On macOS a redraw requested from another thread is dispatched
23//! synchronously onto the main thread, so a wake made off it would wait for
24//! the main thread, holding the lock that makes disconnecting exact, and a
25//! main thread taking that lock meanwhile would deadlock. A draw wake made
26//! off the main thread there never asks winit itself: it posts through the
27//! *off-main route* teksilo-app installs, or, with none installed, hands the
28//! request to the main dispatch queue and returns.
29//!
30//! [`disconnect`](WindowWakeTarget::disconnect) ends it: once it has returned,
31//! the target never asks winit for a redraw again, whoever still holds a
32//! waker. A wake already past its check may still post through a route,
33//! which an event loop that has ended refuses. The window's `Drop` runs it,
34//! and teksilo-app runs it for every window when the event loop ends, since
35//! winit panics on X11 when a window is asked for a redraw after that.
36
37use std::sync::{Arc, OnceLock};
38use std::thread::ThreadId;
39
40use teksilo_canvas::sync::{AtomicBool, AtomicU64, Mutex, Ordering};
41use teksilo_canvas::wake::{RedrawWaker, WakeFlag, WakeGate, WakeKind};
42
43/// The one call a wake target makes into winit, a seam so tests can count it.
44pub(crate) trait RequestRedraw: Send + Sync {
45    fn request_redraw(&self);
46}
47
48impl RequestRedraw for winit::window::Window {
49    fn request_redraw(&self) {
50        winit::window::Window::request_redraw(self);
51    }
52}
53
54/// A route that posts to the event loop. Set once by teksilo-app.
55pub(crate) type WakeRoute = Arc<dyn Fn() + Send + Sync>;
56
57/// One window's wake counters. Monotonic.
58#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
59#[non_exhaustive]
60pub struct LiveWakeStats {
61    /// Wakes of either kind that reached the window, each one counted:
62    /// asked winit for a redraw, or posted to the event loop, where a burst
63    /// is one post.
64    pub wakes: u64,
65    /// Draw wakes dropped because the window was hidden. One redraw is
66    /// requested when it is shown again.
67    pub dropped_hidden: u64,
68    /// Whether the window is hidden, as teksilo-app last said.
69    pub hidden: bool,
70}
71
72/// See the [module docs](self).
73pub(crate) struct WindowWakeTarget {
74    /// A fast path for the check `slot` makes authoritative.
75    connected: AtomicBool,
76    /// The window to ask for a redraw; emptied only by `disconnect`. Held
77    /// across the request, which is what makes "no call after disconnect"
78    /// exact. A leaf: nothing else is locked while it is held.
79    slot: Mutex<Option<Arc<dyn RequestRedraw>>>,
80    /// The thread that created the window, which winit requires to be the
81    /// event loop's.
82    main_thread: ThreadId,
83    state_route: OnceLock<WakeRoute>,
84    off_main_route: OnceLock<WakeRoute>,
85    gate: WakeGate,
86    /// A state wake was posted and the event loop has not taken it yet.
87    state_posted: WakeFlag,
88    /// An off-main draw wake was posted and the event loop has not taken it.
89    draw_posted: WakeFlag,
90    wakes: AtomicU64,
91    dropped_hidden: AtomicU64,
92    /// Itself, for the request a macOS draw wake made off the main thread
93    /// hands to the main queue when no off-main route is installed.
94    #[cfg(target_os = "macos")]
95    this: std::sync::Weak<Self>,
96}
97
98impl WindowWakeTarget {
99    /// A connected target for `window`, created on the window's thread.
100    pub(crate) fn new(window: Arc<dyn RequestRedraw>) -> Arc<Self> {
101        Arc::new_cyclic(|_this| Self {
102            connected: AtomicBool::new(true),
103            slot: Mutex::new(Some(window)),
104            main_thread: std::thread::current().id(),
105            state_route: OnceLock::new(),
106            off_main_route: OnceLock::new(),
107            gate: WakeGate::new(),
108            state_posted: WakeFlag::new(),
109            draw_posted: WakeFlag::new(),
110            wakes: AtomicU64::new(0),
111            dropped_hidden: AtomicU64::new(0),
112            #[cfg(target_os = "macos")]
113            this: _this.clone(),
114        })
115    }
116
117    /// Install the route state wakes post through. The first call wins.
118    pub(crate) fn set_state_route(&self, route: WakeRoute) {
119        let _ = self.state_route.set(route);
120    }
121
122    /// Install the route draw wakes made off the main thread post through.
123    /// The first call wins.
124    pub(crate) fn set_off_main_route(&self, route: WakeRoute) {
125        let _ = self.off_main_route.set(route);
126    }
127
128    /// A draw wake: dropped while hidden, merged into a pending redraw.
129    pub(crate) fn wake_draw(&self) {
130        if !self.connected.load(Ordering::Acquire) {
131            return;
132        }
133        if !self.gate.pass() {
134            self.dropped_hidden.fetch_add(1, Ordering::Relaxed);
135            return;
136        }
137        self.wakes.fetch_add(1, Ordering::Relaxed);
138        if std::thread::current().id() != self.main_thread {
139            if let Some(route) = self.off_main_route.get() {
140                if self.draw_posted.raise() {
141                    route();
142                }
143                return;
144            }
145            #[cfg(target_os = "macos")]
146            {
147                self.request_on_main_queue();
148                return;
149            }
150        }
151        self.request_on_slot();
152    }
153
154    /// macOS, off the main thread, with no off-main route: hand the request
155    /// to the main dispatch queue and return, once per burst. It runs on the
156    /// main thread, where asking winit waits for nobody, and after a
157    /// `disconnect` (also made there) it finds the slot empty.
158    #[cfg(target_os = "macos")]
159    fn request_on_main_queue(&self) {
160        if !self.draw_posted.raise() {
161            return;
162        }
163        let this = self.this.clone();
164        dispatch2::DispatchQueue::main().exec_async(move || {
165            if let Some(target) = this.upgrade() {
166                target.draw_posted.take();
167                target.request_on_slot();
168            }
169        });
170    }
171
172    /// A state wake: never dropped, posted once per burst when a route is
173    /// installed, a redraw request otherwise.
174    pub(crate) fn wake_state(&self) {
175        if !self.connected.load(Ordering::Acquire) {
176            return;
177        }
178        self.wakes.fetch_add(1, Ordering::Relaxed);
179        match self.state_route.get() {
180            Some(route) => {
181                if self.state_posted.raise() {
182                    route();
183                }
184            }
185            None => self.request_on_slot(),
186        }
187    }
188
189    /// Event loop, on receipt of a posted wake of `kind`, *before* acting on
190    /// it: the next wake of that kind posts again. Returns whether one was
191    /// pending.
192    pub(crate) fn take_posted(&self, kind: WakeKind) -> bool {
193        match kind {
194            WakeKind::Draw => self.draw_posted.take(),
195            _ => self.state_posted.take(),
196        }
197    }
198
199    /// Event loop: `true` while hidden, in which case the redraw the caller
200    /// wanted is owed when the window is shown again.
201    pub(crate) fn defer_until_shown(&self) -> bool {
202        if self.gate.pass() {
203            false
204        } else {
205            self.dropped_hidden.fetch_add(1, Ordering::Relaxed);
206            true
207        }
208    }
209
210    /// Event loop: record whether the window is hidden. Showing it requests
211    /// the one redraw owed for the draw wakes dropped meanwhile, if any.
212    pub(crate) fn set_hidden(&self, hidden: bool) {
213        if hidden {
214            self.gate.hide();
215        } else if self.gate.show() && self.connected.load(Ordering::Acquire) {
216            self.wakes.fetch_add(1, Ordering::Relaxed);
217            self.request_on_slot();
218        }
219    }
220
221    /// Event loop: the window draws again and a frame is about to run, which
222    /// serves the draw wakes dropped while it was hidden. Record it shown,
223    /// without the redraw [`set_hidden`](Self::set_hidden) would deliver for
224    /// them.
225    pub(crate) fn show_for_drawing(&self) {
226        let _ = self.gate.show();
227    }
228
229    pub(crate) fn is_hidden(&self) -> bool {
230        self.gate.is_hidden()
231    }
232
233    /// Never call into winit again. Waits for a request already in progress;
234    /// idempotent. The window reference is dropped on the calling thread,
235    /// after the lock is released.
236    pub(crate) fn disconnect(&self) {
237        self.connected.store(false, Ordering::Release);
238        let window = self
239            .slot
240            .lock()
241            .unwrap_or_else(std::sync::PoisonError::into_inner)
242            .take();
243        drop(window);
244    }
245
246    /// Ask winit for a redraw, unless disconnected.
247    pub(crate) fn request_on_slot(&self) {
248        let slot = self
249            .slot
250            .lock()
251            .unwrap_or_else(std::sync::PoisonError::into_inner);
252        if let Some(window) = slot.as_ref() {
253            window.request_redraw();
254        }
255    }
256
257    pub(crate) fn stats(&self) -> LiveWakeStats {
258        LiveWakeStats {
259            wakes: self.wakes.load(Ordering::Relaxed),
260            dropped_hidden: self.dropped_hidden.load(Ordering::Relaxed),
261            hidden: self.gate.is_hidden(),
262        }
263    }
264
265    /// Hold the slot's lock, as an in-progress request would.
266    #[cfg(all(test, not(teksilo_loom)))]
267    fn hold_slot(&self) -> impl Drop + '_ {
268        self.slot
269            .lock()
270            .unwrap_or_else(std::sync::PoisonError::into_inner)
271    }
272}
273
274impl RedrawWaker for WindowWakeTarget {
275    fn wake(&self, kind: WakeKind) {
276        match kind {
277            WakeKind::Draw => self.wake_draw(),
278            // Every other kind keeps state current, and is never dropped.
279            _ => self.wake_state(),
280        }
281    }
282
283    fn is_hidden(&self) -> bool {
284        self.gate.is_hidden()
285    }
286}
287
288impl std::fmt::Debug for WindowWakeTarget {
289    /// Atomics only: never takes the slot's lock.
290    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291        f.debug_struct("WindowWakeTarget")
292            .field("connected", &self.connected.load(Ordering::Relaxed))
293            .field("gate", &self.gate)
294            .field("stats", &self.stats())
295            .finish_non_exhaustive()
296    }
297}
298
299#[cfg(all(test, not(teksilo_loom)))]
300mod tests;
301
302#[cfg(all(test, teksilo_loom))]
303mod loom_tests;