Skip to main content

day_core/
frame.rs

1// Copyright © The Daybrite Project
2// SPDX-License-Identifier: MPL-2.0
3
4//! Window-scoped display scheduling (docs/frames.md). Native frame requests are shared by
5//! one-shot clients, continuous subscriptions and the legacy `frame_clock` piece. No timers,
6//! no implicit physics clamp, and no native request when there is no demand.
7
8use std::cell::RefCell;
9use std::collections::HashMap;
10use std::marker::PhantomData;
11use std::ops::ControlFlow;
12use std::rc::Rc;
13use std::time::Duration;
14
15use crate::tree::RNode;
16use day_spec::{CancelFrame, FrameCallback, FrameStamp};
17
18/// A frame opportunity, not confirmation that pixels were presented. All clients of a window
19/// receive the same timestamp. `delta` belongs to this subscription: zero on its first frame
20/// and after an explicit pause/resume. It is not clamped; simulation clients choose their policy.
21#[derive(Clone, Copy, Debug, PartialEq)]
22pub struct Frame {
23    pub timestamp: Duration,
24    pub delta: Duration,
25    pub target_timestamp: Option<Duration>,
26}
27
28type Callback = Rc<RefCell<Box<dyn FnMut(Frame) -> ControlFlow<()>>>>;
29type Requester = Rc<dyn Fn(RNode, FrameCallback) -> CancelFrame>;
30struct Consumer {
31    root: RNode,
32    cb: Callback,
33    active: bool,
34    generation: u64,
35    last: Option<f64>,
36}
37#[derive(Default)]
38struct Source {
39    ticket: u64,
40    armed: bool,
41    dispatching: bool,
42    cancel: Option<CancelFrame>,
43}
44#[derive(Default)]
45struct Driver {
46    requester: Option<Requester>,
47    consumers: HashMap<u64, Consumer>,
48    sources: HashMap<RNode, Source>,
49    next_id: u64,
50    suspended: bool,
51}
52
53day_reactive::tls_slots! {
54    frame;
55    static DRIVER: RefCell<Driver> = RefCell::new(Driver::default());
56    static NATIVE_CALLBACKS: RefCell<(u64, HashMap<u64, FrameCallback>)> = RefCell::new((0, HashMap::new()));
57    /// The first and the latest frame timestamp seen (ms), for `uptime_secs` where the
58    /// process has no clock of its own (wasm).
59    static STAMPS: std::cell::Cell<Option<(f64, f64)>> = std::cell::Cell::new(None);
60}
61
62/// How long the app has run, from the frame clock: the span between the first and the latest
63/// frame timestamp a backend delivered. The exit line uses it on wasm, where `Instant` does not
64/// exist; `None` before any frame.
65pub fn uptime_secs() -> Option<u64> {
66    STAMPS.with(|s| {
67        s.get()
68            .map(|(first, last)| ((last - first).max(0.0) / 1000.0) as u64)
69    })
70}
71
72/// Install the native scheduler at launch. The requester MUST deliver asynchronously; tests may
73/// retain the callback and deliver chosen timestamps manually. Replaces no live registrations.
74pub fn install_frame_requester(f: impl Fn(RNode, FrameCallback) -> CancelFrame + 'static) {
75    DRIVER.with(|d| d.borrow_mut().requester = Some(Rc::new(f)));
76}
77
78/// A reusable source bound to one window. Capture `current()` while building a page, then use it
79/// from input handlers or other UI-thread code. A closed window cancels all its registrations.
80#[derive(Clone, Copy, Debug)]
81pub struct FrameClock {
82    root: RNode,
83    ui_thread: PhantomData<Rc<()>>,
84}
85impl FrameClock {
86    /// The current page's window (the initial window outside a page build).
87    pub fn current() -> Self {
88        Self::for_root(crate::toolbar::current_page_window())
89    }
90    pub(crate) fn for_root(root: RNode) -> Self {
91        Self {
92            root,
93            ui_thread: PhantomData,
94        }
95    }
96    /// Run once at the next frame opportunity. Retain the returned handle until delivery;
97    /// dropping it cancels. Requests made inside a callback run on a subsequent frame.
98    pub fn request(self, cb: impl FnOnce(Frame) + 'static) -> FrameHandle {
99        let mut cb = Some(cb);
100        self.subscribe(move |frame| {
101            if let Some(cb) = cb.take() {
102                cb(frame);
103            }
104            ControlFlow::Break(())
105        })
106    }
107    /// Run while the callback returns `Continue(())`. `Break(())` pauses the subscription;
108    /// `resume()` can start it again. Drop or `cancel()` removes it permanently.
109    pub fn subscribe(self, cb: impl FnMut(Frame) -> ControlFlow<()> + 'static) -> FrameHandle {
110        let id = DRIVER.with(|d| {
111            let mut d = d.borrow_mut();
112            d.next_id += 1;
113            let id = d.next_id;
114            d.consumers.insert(
115                id,
116                Consumer {
117                    root: self.root,
118                    cb: Rc::new(RefCell::new(Box::new(cb))),
119                    active: true,
120                    generation: 0,
121                    last: None,
122                },
123            );
124            id
125        });
126        arm(self.root);
127        FrameHandle {
128            id,
129            ui_thread: PhantomData,
130        }
131    }
132}
133
134/// Request one frame in the current window. Prefer a captured [`FrameClock`] from event handlers.
135pub fn request(cb: impl FnOnce(Frame) + 'static) -> FrameHandle {
136    FrameClock::current().request(cb)
137}
138/// Subscribe in the current window. Dropping the returned handle cancels the subscription.
139pub fn subscribe(cb: impl FnMut(Frame) -> ControlFlow<()> + 'static) -> FrameHandle {
140    FrameClock::current().subscribe(cb)
141}
142
143/// Owns a registration. UI-thread only. Keep it alive for as long as the callback is wanted.
144#[must_use = "dropping the frame handle cancels the callback"]
145pub struct FrameHandle {
146    id: u64,
147    ui_thread: PhantomData<Rc<()>>,
148}
149impl FrameHandle {
150    pub fn pause(&self) {
151        set_active(self.id, false);
152    }
153    pub fn resume(&self) {
154        set_active(self.id, true);
155    }
156    pub fn is_active(&self) -> bool {
157        DRIVER.with(|d| d.borrow().consumers.get(&self.id).is_some_and(|c| c.active))
158    }
159    pub fn cancel(&self) {
160        remove(self.id);
161    }
162    /// Keep the registration for this reactive scope; dispose cancels it. This is optional:
163    /// clients without a mounted piece can own the handle directly.
164    pub fn in_scope(self) {
165        day_reactive::Scope::current().on_cleanup(move || drop(self));
166    }
167}
168impl Drop for FrameHandle {
169    fn drop(&mut self) {
170        self.cancel();
171    }
172}
173
174fn set_active(id: u64, active: bool) {
175    let root = DRIVER.with(|d| {
176        let mut d = d.borrow_mut();
177        let c = d.consumers.get_mut(&id)?;
178        if c.active != active {
179            c.active = active;
180            c.generation += 1;
181            c.last = None;
182        }
183        Some(c.root)
184    });
185    if let Some(root) = root {
186        reconcile(root);
187    }
188}
189fn remove(id: u64) {
190    // User closures can own other handles. Drop them outside the driver borrow so their
191    // destructors can cancel those registrations without re-entering a borrowed RefCell.
192    // Native Quit can leave pages/callback captures alive until core TLS destruction.
193    // Their handles must not re-enter the dying root; its registries already own cleanup.
194    let Ok(removed) = DRIVER.try_with(|d| d.borrow_mut().consumers.remove(&id)) else {
195        return;
196    };
197    let root = removed.as_ref().map(|c| c.root);
198    drop(removed);
199    if let Some(root) = root {
200        reconcile(root);
201    }
202}
203fn reconcile(root: RNode) {
204    let cancel = DRIVER.with(|d| {
205        let mut d = d.borrow_mut();
206        let active = !d.suspended && d.consumers.values().any(|c| c.root == root && c.active);
207        if active {
208            return None;
209        }
210        let source = d.sources.get_mut(&root)?;
211        source.ticket += 1;
212        source.armed = false;
213        source.cancel.take()
214    });
215    if let Some(cancel) = cancel {
216        cancel();
217    }
218    arm(root);
219}
220fn arm(root: RNode) {
221    let request = DRIVER.with(|d| {
222        let mut d = d.borrow_mut();
223        if d.suspended || !d.consumers.values().any(|c| c.root == root && c.active) {
224            return None;
225        }
226        let requester = d.requester.clone()?;
227        let source = d.sources.entry(root).or_default();
228        if source.armed || source.dispatching {
229            return None;
230        }
231        source.ticket += 1;
232        source.armed = true;
233        Some((requester, source.ticket))
234    });
235    if let Some((requester, ticket)) = request {
236        let cancel = requester(root, Box::new(move |stamp| tick(root, ticket, stamp)));
237        DRIVER.with(|d| {
238            if let Some(s) = d.borrow_mut().sources.get_mut(&root) {
239                s.cancel = Some(cancel);
240            }
241        });
242    }
243}
244fn tick(root: RNode, ticket: u64, stamp: FrameStamp) {
245    STAMPS.with(|s| {
246        let first = s.get().map_or(stamp.timestamp, |(first, _)| first);
247        s.set(Some((first, stamp.timestamp)));
248    });
249    let callbacks = DRIVER.with(|d| {
250        let mut d = d.borrow_mut();
251        let Some(source) = d.sources.get_mut(&root) else {
252            return Vec::new();
253        };
254        if !source.armed || source.ticket != ticket {
255            return Vec::new();
256        }
257        source.armed = false;
258        source.cancel.take();
259        if Duration::try_from_secs_f64(stamp.timestamp).is_err() {
260            return Vec::new();
261        }
262        source.dispatching = true;
263        let mut callbacks: Vec<_> = d
264            .consumers
265            .iter()
266            .filter(|(_, c)| c.root == root && c.active)
267            .map(|(&id, c)| (id, c.generation, c.cb.clone()))
268            .collect();
269        callbacks.sort_by_key(|c| c.0);
270        callbacks
271    });
272    day_reactive::batch(|| {
273        for (id, generation, cb) in callbacks {
274            let frame = DRIVER.with(|d| {
275                let mut d = d.borrow_mut();
276                if d.suspended {
277                    return None;
278                }
279                let c = d.consumers.get_mut(&id)?;
280                if !c.active || c.generation != generation {
281                    return None;
282                }
283                // Ignore backward stamps without moving the baseline backwards.
284                let ts = stamp.timestamp.max(c.last.unwrap_or(stamp.timestamp));
285                let delta = c.last.map_or(0.0, |last| ts - last);
286                c.last = Some(ts);
287                Some(Frame {
288                    timestamp: Duration::from_secs_f64(ts),
289                    delta: Duration::from_secs_f64(delta),
290                    target_timestamp: stamp
291                        .target_timestamp
292                        .filter(|t| t.is_finite() && *t >= ts)
293                        .and_then(|t| Duration::try_from_secs_f64(t).ok()),
294                })
295            });
296            let Some(frame) = frame else {
297                continue;
298            };
299            let flow = day_spec::ffi_guard::contain(ControlFlow::Break(()), || {
300                cb.try_borrow_mut()
301                    .map_or(ControlFlow::Break(()), |mut f| f(frame))
302            });
303            if flow.is_break() {
304                DRIVER.with(|d| {
305                    if let Some(c) = d.borrow_mut().consumers.get_mut(&id)
306                        && c.generation == generation
307                    {
308                        c.active = false;
309                        c.last = None;
310                    }
311                });
312            }
313        }
314    });
315    day_reactive::flush_now();
316    DRIVER.with(|d| {
317        if let Some(source) = d.borrow_mut().sources.get_mut(&root) {
318            source.dispatching = false;
319        }
320    });
321    reconcile(root);
322}
323
324/// Suspend display work when the app backgrounds; foreground resumes demand with a fresh delta.
325/// Desktop loss of keyboard focus alone does not suspend visible windows.
326pub(crate) fn lifecycle(phase: day_spec::Lifecycle) {
327    match phase {
328        day_spec::Lifecycle::DidEnterBackground => suspend(true),
329        day_spec::Lifecycle::WillEnterForeground | day_spec::Lifecycle::DidBecomeActive => {
330            suspend(false)
331        }
332        _ => {}
333    }
334}
335fn suspend(suspended: bool) {
336    let roots = DRIVER.with(|d| {
337        let mut d = d.borrow_mut();
338        if d.suspended == suspended {
339            return Vec::new();
340        }
341        d.suspended = suspended;
342        for c in d.consumers.values_mut() {
343            c.last = None;
344            c.generation += 1;
345        }
346        // A first subscription may have been created while suspended, before that window
347        // ever acquired a native source. Include its demand when foregrounding too.
348        d.sources
349            .keys()
350            .copied()
351            .chain(d.consumers.values().map(|c| c.root))
352            .collect::<std::collections::HashSet<_>>()
353            .into_iter()
354            .collect::<Vec<_>>()
355    });
356    for root in roots {
357        reconcile(root);
358    }
359}
360pub(crate) fn forget_window(root: RNode) {
361    let (cancel, removed) = DRIVER.with(|d| {
362        let mut d = d.borrow_mut();
363        let ids: Vec<_> = d
364            .consumers
365            .iter()
366            .filter(|(_, c)| c.root == root)
367            .map(|(&id, _)| id)
368            .collect();
369        let removed: Vec<_> = ids
370            .into_iter()
371            .filter_map(|id| d.consumers.remove(&id))
372            .collect();
373        (d.sources.remove(&root).and_then(|s| s.cancel), removed)
374    });
375    drop(removed);
376    if let Some(cancel) = cancel {
377        cancel();
378    }
379}
380
381/// Legacy `frame_clock` registration. New code should own a [`FrameHandle`].
382#[derive(Clone, Copy, Debug, PartialEq, Eq)]
383pub struct FrameConsumer(u64);
384pub fn add_frame_consumer(mut cb: impl FnMut(Duration) + 'static) -> FrameConsumer {
385    let mut first = true;
386    let handle = subscribe(move |frame| {
387        // Preserve the original first-frame and <=100ms simulation contract for existing games.
388        cb(if std::mem::take(&mut first) {
389            Duration::from_secs_f64(1.0 / 60.0)
390        } else {
391            frame.delta.min(Duration::from_millis(100))
392        });
393        ControlFlow::Continue(())
394    });
395    let id = handle.id;
396    std::mem::forget(handle);
397    FrameConsumer(id)
398}
399pub fn remove_frame_consumer(c: FrameConsumer) {
400    remove(c.0);
401}
402/// Number of active subscriptions and pending single-frame clients (paused handles excluded).
403pub fn frame_consumer_count() -> usize {
404    DRIVER.with(|d| d.borrow().consumers.values().filter(|c| c.active).count())
405}
406
407/// Backend callback registry. Native APIs carry integer tickets, never pointers to Rust closures:
408/// a cancelled or late native callback cannot dereference freed memory. UI-thread only.
409#[doc(hidden)]
410pub mod native {
411    use super::*;
412    pub fn register(cb: FrameCallback) -> u64 {
413        NATIVE_CALLBACKS.with(|c| {
414            let mut c = c.borrow_mut();
415            c.0 += 1;
416            let id = c.0;
417            c.1.insert(id, cb);
418            id
419        })
420    }
421    pub fn cancel(id: u64) {
422        NATIVE_CALLBACKS.with(|c| c.borrow_mut().1.remove(&id));
423    }
424    pub fn deliver(id: u64, stamp: FrameStamp) {
425        let cb = NATIVE_CALLBACKS.with(|c| c.borrow_mut().1.remove(&id));
426        if let Some(cb) = cb {
427            day_spec::ffi_guard::contain((), || cb(stamp));
428        }
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use std::cell::Cell;
436    use std::collections::VecDeque;
437
438    struct Manual {
439        pending: Rc<RefCell<VecDeque<(RNode, FrameCallback)>>>,
440        requests: Rc<Cell<usize>>,
441        cancellations: Rc<Cell<usize>>,
442    }
443    impl Manual {
444        fn new() -> Self {
445            DRIVER.with(|d| *d.borrow_mut() = Driver::default());
446            let pending = Rc::new(RefCell::new(VecDeque::new()));
447            let requests = Rc::new(Cell::new(0));
448            let cancellations = Rc::new(Cell::new(0));
449            let (q, r, c) = (pending.clone(), requests.clone(), cancellations.clone());
450            install_frame_requester(move |root, cb| {
451                r.set(r.get() + 1);
452                q.borrow_mut().push_back((root, cb));
453                let c = c.clone();
454                Box::new(move || c.set(c.get() + 1))
455            });
456            Self {
457                pending,
458                requests,
459                cancellations,
460            }
461        }
462        fn fire(&self, ts: f64) {
463            let (_, cb) = self
464                .pending
465                .borrow_mut()
466                .pop_front()
467                .expect("pending frame");
468            cb(FrameStamp {
469                timestamp: ts,
470                target_timestamp: Some(ts + 1.0 / 120.0),
471            });
472        }
473        fn clock(&self) -> FrameClock {
474            FrameClock::for_root(RNode::default())
475        }
476    }
477    #[test]
478    fn clients_share_one_request_and_idle_after_delivery() {
479        let m = Manual::new();
480        let seen = Rc::new(RefCell::new(Vec::new()));
481        let s = seen.clone();
482        let a = m.clock().request(move |f| s.borrow_mut().push(f));
483        let s = seen.clone();
484        let b = m.clock().request(move |f| s.borrow_mut().push(f));
485        assert_eq!(m.requests.get(), 1);
486        m.fire(10.0);
487        assert_eq!(seen.borrow().len(), 2);
488        assert_eq!(seen.borrow()[0], seen.borrow()[1]);
489        assert_eq!(seen.borrow()[0].delta, Duration::ZERO);
490        assert!(seen.borrow()[0].target_timestamp.is_some());
491        assert!(!a.is_active() && !b.is_active());
492        assert!(m.pending.borrow().is_empty());
493    }
494    #[test]
495    fn dropping_last_client_cancels_and_late_delivery_is_harmless() {
496        let m = Manual::new();
497        let a = m.clock().request(|_| panic!("cancelled callback ran"));
498        drop(a);
499        assert_eq!(m.cancellations.get(), 1);
500        m.fire(10.0); // emulate a callback already queued before native cancellation
501        assert_eq!(frame_consumer_count(), 0);
502        assert_eq!(m.requests.get(), 1);
503    }
504    #[test]
505    fn cancellation_during_dispatch_skips_the_later_callback() {
506        let m = Manual::new();
507        let later = Rc::new(RefCell::new(None::<FrameHandle>));
508        let l = later.clone();
509        let _first = m.clock().request(move |_| {
510            l.borrow_mut().take();
511        });
512        *later.borrow_mut() = Some(m.clock().request(|_| panic!("cancelled during dispatch")));
513        m.fire(1.0);
514        assert!(m.pending.borrow().is_empty());
515    }
516    #[test]
517    fn requests_from_a_callback_run_on_the_next_frame() {
518        let m = Manual::new();
519        let later = Rc::new(RefCell::new(None::<FrameHandle>));
520        let l = later.clone();
521        let seen = Rc::new(Cell::new(false));
522        let s = seen.clone();
523        let clock = m.clock();
524        let _first = clock.request(move |_| {
525            *l.borrow_mut() = Some(clock.request(move |_| s.set(true)));
526        });
527        m.fire(1.0);
528        assert!(!seen.get());
529        assert_eq!(m.requests.get(), 2);
530        m.fire(2.0);
531        assert!(seen.get());
532    }
533    #[test]
534    fn timestamps_are_unclamped_and_resume_starts_a_new_baseline() {
535        let m = Manual::new();
536        let seen = Rc::new(RefCell::new(Vec::new()));
537        let s = seen.clone();
538        let handle = m.clock().subscribe(move |f| {
539            s.borrow_mut().push(f.delta);
540            ControlFlow::Continue(())
541        });
542        m.fire(1.0);
543        m.fire(1.75);
544        assert_eq!(seen.borrow()[1], Duration::from_millis(750));
545        handle.pause();
546        m.fire(2.0); // cancelled pending frame
547        handle.resume();
548        m.fire(50.0);
549        assert_eq!(seen.borrow()[2], Duration::ZERO);
550    }
551    #[test]
552    fn stopping_then_waking_a_subscription_does_not_poll() {
553        let m = Manual::new();
554        let calls = Rc::new(Cell::new(0));
555        let c = calls.clone();
556        let handle = m.clock().subscribe(move |_| {
557            c.set(c.get() + 1);
558            ControlFlow::Break(())
559        });
560        m.fire(1.0);
561        assert!(!handle.is_active());
562        assert!(m.pending.borrow().is_empty());
563        handle.resume();
564        handle.resume();
565        assert_eq!(m.requests.get(), 2);
566        m.fire(5.0);
567        assert_eq!(calls.get(), 2);
568    }
569    #[test]
570    fn windows_have_independent_native_sources() {
571        let m = Manual::new();
572        let mut roots = slotmap::SlotMap::<RNode, ()>::with_key();
573        let one = roots.insert(());
574        let two = roots.insert(());
575        let _a = FrameClock::for_root(one).request(|_| {});
576        let b = FrameClock::for_root(two).subscribe(|_| ControlFlow::Continue(()));
577        assert_eq!(m.requests.get(), 2);
578        assert_eq!(m.pending.borrow()[0].0, one);
579        assert_eq!(m.pending.borrow()[1].0, two);
580        forget_window(two);
581        assert!(!b.is_active());
582        m.fire(1.0);
583        m.fire(1.0);
584        assert!(m.pending.borrow().is_empty());
585    }
586    #[test]
587    fn background_suspends_and_foreground_resets_delta() {
588        let m = Manual::new();
589        let seen = Rc::new(RefCell::new(Vec::new()));
590        let s = seen.clone();
591        let _handle = m.clock().subscribe(move |f| {
592            s.borrow_mut().push(f.delta);
593            ControlFlow::Continue(())
594        });
595        m.fire(1.0);
596        lifecycle(day_spec::Lifecycle::DidEnterBackground);
597        m.fire(2.0);
598        assert_eq!(seen.borrow().len(), 1);
599        lifecycle(day_spec::Lifecycle::WillEnterForeground);
600        m.fire(80.0);
601        assert_eq!(seen.borrow()[1], Duration::ZERO);
602    }
603    #[test]
604    fn malformed_and_backward_stamps_do_not_poison_the_clock() {
605        let m = Manual::new();
606        let seen = Rc::new(RefCell::new(Vec::new()));
607        let s = seen.clone();
608        let _handle = m.clock().subscribe(move |f| {
609            s.borrow_mut().push(f.delta);
610            ControlFlow::Continue(())
611        });
612        m.fire(f64::NAN);
613        m.fire(5.0);
614        m.fire(4.0);
615        m.fire(5.25);
616        assert_eq!(
617            *seen.borrow(),
618            vec![Duration::ZERO, Duration::ZERO, Duration::from_millis(250)]
619        );
620    }
621
622    #[test]
623    fn first_subscription_created_in_background_starts_on_foreground() {
624        let m = Manual::new();
625        lifecycle(day_spec::Lifecycle::DidEnterBackground);
626        let seen = Rc::new(Cell::new(false));
627        let s = seen.clone();
628        let _handle = m.clock().request(move |frame| {
629            assert_eq!(frame.delta, Duration::ZERO);
630            s.set(true);
631        });
632        assert_eq!(m.requests.get(), 0);
633        lifecycle(day_spec::Lifecycle::WillEnterForeground);
634        assert_eq!(m.requests.get(), 1);
635        m.fire(20.0);
636        assert!(seen.get());
637    }
638    #[test]
639    fn cancelled_native_tickets_can_never_run_a_new_callback() {
640        let calls = Rc::new(Cell::new(0));
641        let first = native::register(Box::new(|_| panic!("stale native ticket")));
642        native::cancel(first);
643        let c = calls.clone();
644        let second = native::register(Box::new(move |_| c.set(c.get() + 1)));
645        native::deliver(first, FrameStamp::new(1.0));
646        native::deliver(second, FrameStamp::new(1.0));
647        native::deliver(second, FrameStamp::new(2.0));
648        assert_eq!(calls.get(), 1);
649    }
650
651    #[test]
652    fn dropping_callbacks_can_cancel_other_handles() {
653        let m = Manual::new();
654        let inner = m.clock().request(|_| panic!("cancelled inner callback"));
655        let outer = m.clock().subscribe(move |_| {
656            let _ = &inner;
657            ControlFlow::Continue(())
658        });
659        drop(outer);
660        assert_eq!(frame_consumer_count(), 0);
661        m.fire(1.0);
662    }
663
664    #[test]
665    fn closing_a_window_drops_nested_handles_outside_the_driver_borrow() {
666        let m = Manual::new();
667        let inner = m.clock().request(|_| panic!("closed window"));
668        let _outer = m.clock().subscribe(move |_| {
669            let _ = &inner;
670            ControlFlow::Continue(())
671        });
672        forget_window(m.clock().root);
673        assert_eq!(frame_consumer_count(), 0);
674        m.fire(1.0);
675    }
676
677    #[test]
678    fn handles_drop_during_core_tls_teardown() {
679        const CHILD: &str = "DAY_CORE_FRAME_TEARDOWN_CHILD";
680        if std::env::var_os(CHILD).is_some() {
681            struct TeardownHandle {
682                handle: Option<FrameHandle>,
683                dropped: std::sync::mpsc::Sender<()>,
684            }
685            impl Drop for TeardownHandle {
686                fn drop(&mut self) {
687                    assert!(crate::try_tls_root(|_| ()).is_err());
688                    drop(self.handle.take());
689                    self.dropped.send(()).unwrap();
690                }
691            }
692            let (dropped, observed) = std::sync::mpsc::channel();
693            std::thread::spawn(move || {
694                let m = Manual::new();
695                let paused = m.clock().subscribe(|_| ControlFlow::Continue(()));
696                paused.pause();
697                let held = TeardownHandle {
698                    handle: Some(paused),
699                    dropped: dropped.clone(),
700                };
701                let active = m.clock().subscribe(move |_| {
702                    let _ = &held;
703                    ControlFlow::Continue(())
704                });
705                let held = TeardownHandle {
706                    handle: Some(active),
707                    dropped,
708                };
709                // The driver owns the paused handle's capture; the native registry owns
710                // the active handle's capture. Both drop with core TLS unavailable, before
711                // and after the driver finishes destruction, regardless of TLS key order.
712                native::register(Box::new(move |_| drop(held)));
713            })
714            .join()
715            .unwrap();
716            for _ in 0..2 {
717                observed
718                    .recv_timeout(Duration::from_secs(5))
719                    .expect("frame handle was not dropped during thread shutdown");
720            }
721            return;
722        }
723        // A panic in a TLS destructor aborts the process, so isolate the regression.
724        let output = std::process::Command::new(std::env::current_exe().unwrap())
725            .args([
726                "--exact",
727                "frame::tests::handles_drop_during_core_tls_teardown",
728                "--nocapture",
729            ])
730            .env(CHILD, "1")
731            .output()
732            .unwrap();
733        assert!(
734            output.status.success(),
735            "{}",
736            String::from_utf8_lossy(&output.stderr)
737        );
738    }
739}