Skip to main content

ferroui_base/rendering/composition/
composition_batch.rs

1//! Completion of groups of animations: WinUI's `CompositionScopedBatch` and
2//! `CompositionCommitBatch`, with the `Compositor` factories
3//! `CreateScopedBatch` and `GetCommitBatch` (and `CreatePropertySet`).
4//!
5//! # How completion is tracked
6//!
7//! A batch is a counter shared between the threads (`BatchShared`, `Send +
8//! Sync`): the number of animations of the batch that are still running, and
9//! whether the batch is ended. Every key frame animation that is started while
10//! batches are open takes a [`BatchCompletionGuard`] of each of them (gathered
11//! in an [`AnimationCompletion`]); the guard travels with the factory of the
12//! animation instance to the server and is released there when the animation
13//! finishes, or when the instance is dropped (stopped, replaced by another
14//! animation of the same property, its target disposed). Releasing is
15//! dropping, so it happens once.
16//!
17//! A batch completes when it is ended and none of its animations is running.
18//! The thread that completes it (the render thread, or this one when the last
19//! guard is dropped here) cannot touch the UI-thread objects: it posts the
20//! completion to the dispatcher of the compositor, which looks the handlers of
21//! the batch up in a registry of the thread by the id of the batch and raises
22//! `Completed`. As in WinUI, `Completed` is raised asynchronously, also for a
23//! batch that is ended with no animations, so a handler subscribed right after
24//! `End` is called.
25//!
26//! The registry holds the handler list of a batch from `End` (for a commit
27//! batch: from `GetCommitBatch`) until completion: the application does not
28//! have to keep the batch alive to get the event, as in WinUI.
29
30use super::{AsCompositionObject, CompositionObject, CompositionPropertySet, Compositor};
31use crate::reactive::{Disposable, IDisposable};
32use crate::threading::{Dispatcher, DispatcherPriority};
33use crate::utilities::HandlerList;
34use bitflags::bitflags;
35use std::cell::{Cell, RefCell};
36use std::collections::HashMap;
37use std::rc::{Rc, Weak};
38use std::sync::atomic::{AtomicU64, Ordering};
39use std::sync::{Arc, Mutex};
40
41bitflags! {
42    /// Batch types for [`CompositionCommitBatch`] and
43    /// [`CompositionScopedBatch`] (WinUI `CompositionBatchTypes`, a flags
44    /// enumeration).
45    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
46    pub struct CompositionBatchTypes: u32 {
47        /// The batch contains animations.
48        const ANIMATION = 1;
49        /// The batch contains effects.
50        const EFFECT = 2;
51        /// The batch contains an infinite animation.
52        const INFINITE_ANIMATION = 4;
53        /// The batch contains all animations.
54        const ALL_ANIMATIONS = 5;
55    }
56}
57
58impl CompositionBatchTypes {
59    /// None.
60    pub const NONE: CompositionBatchTypes = CompositionBatchTypes::empty();
61}
62
63/// The arguments of the `Completed` event of a batch (WinUI
64/// `CompositionBatchCompletedEventArgs`, which has no members of its own).
65#[derive(Clone, Copy, Debug)]
66pub struct CompositionBatchCompletedEventArgs {
67    _private: (),
68}
69
70/// The handlers of the `Completed` event of a batch.
71type CompletedHandlers = HandlerList<dyn Fn(&CompositionBatchCompletedEventArgs)>;
72
73static NEXT_BATCH_ID: AtomicU64 = AtomicU64::new(1);
74
75crate::ferro_thread_local! {
76    /// The handlers of the batches of this thread that are ended and not
77    /// completed yet, by batch id: where a completion posted from another
78    /// thread finds them.
79    static PENDING_COMPLETIONS: RefCell<HashMap<u64, Rc<CompletedHandlers>>> = RefCell::new(HashMap::new());
80}
81
82/// Raises `Completed` of the batch `id`: runs on the thread of the batch,
83/// posted by `BatchShared::check_completed`.
84fn raise_completed(id: u64) {
85    let handlers = PENDING_COMPLETIONS
86        .try_with(|pending| pending.try_borrow_mut().ok().and_then(|mut pending| pending.remove(&id)))
87        .ok()
88        .flatten();
89    let Some(handlers) = handlers else { return };
90    let args = CompositionBatchCompletedEventArgs { _private: () };
91    for (_, handler) in handlers.snapshot().iter() {
92        handler(&args);
93    }
94}
95
96fn register_pending_completion(id: u64, handlers: Rc<CompletedHandlers>) {
97    PENDING_COMPLETIONS.with(|pending| {
98        pending.borrow_mut().insert(id, handlers);
99    });
100}
101
102/// Subscribes `handler` to `handlers`; disposing the result unsubscribes.
103fn subscribe(
104    handlers: &Rc<CompletedHandlers>,
105    handler: impl Fn(&CompositionBatchCompletedEventArgs) + 'static,
106) -> Rc<dyn IDisposable> {
107    let token = handlers.add(Rc::new(handler));
108    let weak = Rc::downgrade(handlers);
109    Disposable::create(move || {
110        if let Some(handlers) = weak.upgrade() {
111            handlers.remove(token);
112        }
113    })
114}
115
116struct BatchState {
117    /// The batch types whose animations the completion waits for.
118    types: CompositionBatchTypes,
119    /// Running animations of the batch that end by themselves.
120    finite: usize,
121    /// Running infinite animations of the batch.
122    infinite: usize,
123    ended: bool,
124    completed: bool,
125    /// Whether a UI-thread batch object waits for the completion: the batch
126    /// of a frame nobody asked for completes silently.
127    observed: bool,
128}
129
130/// The state of a batch that the threads share: what the guards of its
131/// animations count down.
132pub(crate) struct BatchShared {
133    id: u64,
134    dispatcher: Arc<Dispatcher>,
135    state: Mutex<BatchState>,
136}
137
138impl BatchShared {
139    fn new(dispatcher: Arc<Dispatcher>, types: CompositionBatchTypes, observed: bool) -> Arc<BatchShared> {
140        Arc::new(BatchShared {
141            id: NEXT_BATCH_ID.fetch_add(1, Ordering::Relaxed),
142            dispatcher,
143            state: Mutex::new(BatchState {
144                types,
145                finite: 0,
146                infinite: 0,
147                ended: false,
148                completed: false,
149                observed,
150            }),
151        })
152    }
153
154    fn lock(&self) -> std::sync::MutexGuard<'_, BatchState> {
155        self.state.lock().unwrap_or_else(|e| e.into_inner())
156    }
157
158    fn acquire(self: &Arc<Self>, infinite: bool) -> BatchCompletionGuard {
159        {
160            let mut state = self.lock();
161            if infinite {
162                state.infinite += 1;
163            } else {
164                state.finite += 1;
165            }
166        }
167        BatchCompletionGuard { shared: self.clone(), infinite }
168    }
169
170    fn release(&self, infinite: bool) {
171        let mut state = self.lock();
172        if infinite {
173            state.infinite = state.infinite.saturating_sub(1);
174        } else {
175            state.finite = state.finite.saturating_sub(1);
176        }
177        self.check_completed(state);
178    }
179
180    fn end(&self) {
181        let mut state = self.lock();
182        state.ended = true;
183        self.check_completed(state);
184    }
185
186    fn is_ended(&self) -> bool {
187        self.lock().ended
188    }
189
190    /// Marks the batch as observed by a UI-thread object waiting for the
191    /// given types.
192    fn observe(&self, types: CompositionBatchTypes) {
193        let mut state = self.lock();
194        state.observed = true;
195        state.types |= types;
196    }
197
198    /// Completes the batch when it is ended and none of the animations it
199    /// waits for is running, posting `Completed` to the thread of the batch.
200    fn check_completed(&self, mut state: std::sync::MutexGuard<'_, BatchState>) {
201        if state.completed || !state.ended {
202            return;
203        }
204        let mut running = 0;
205        if state.types.contains(CompositionBatchTypes::ANIMATION) {
206            running += state.finite;
207        }
208        if state.types.contains(CompositionBatchTypes::INFINITE_ANIMATION) {
209            running += state.infinite;
210        }
211        if running != 0 {
212            return;
213        }
214        state.completed = true;
215        let observed = state.observed;
216        drop(state);
217        if observed {
218            let id = self.id;
219            self.dispatcher.post(move || raise_completed(id), DispatcherPriority::NORMAL);
220        }
221    }
222}
223
224/// The share of one batch in a running animation: dropping it tells the
225/// batch that the animation is over.
226pub struct BatchCompletionGuard {
227    shared: Arc<BatchShared>,
228    infinite: bool,
229}
230
231impl Drop for BatchCompletionGuard {
232    fn drop(&mut self) {
233        self.shared.release(self.infinite);
234    }
235}
236
237/// The guards of the batches a running animation belongs to. It is `Send`:
238/// it is created on the UI thread with the animation and moved to the
239/// server instance of the animation, which releases it when the animation
240/// is over.
241#[derive(Default)]
242pub struct AnimationCompletion {
243    guards: Vec<BatchCompletionGuard>,
244}
245
246impl AnimationCompletion {
247    /// The completion of an animation that belongs to no batch.
248    pub fn none() -> AnimationCompletion {
249        AnimationCompletion::default()
250    }
251
252    /// Whether the animation belongs to no batch (any more).
253    pub fn is_empty(&self) -> bool {
254        self.guards.is_empty()
255    }
256
257    /// Tells the batches that the animation is over. Releasing again does
258    /// nothing.
259    pub fn release(&mut self) {
260        self.guards.clear();
261    }
262}
263
264/// The batches of a compositor: the scoped batches that are open and the
265/// batch of the frame that is not committed yet.
266#[derive(Default)]
267pub(crate) struct CompositorBatches {
268    scoped: RefCell<Vec<Weak<CompositionScopedBatch>>>,
269    /// The batch of the current frame: created with the first animation of
270    /// the frame (or by `GetCommitBatch`) and ended by the commit.
271    frame: RefCell<Option<Arc<BatchShared>>>,
272    /// The UI-thread object of the batch of the current frame, once asked
273    /// for.
274    commit_batch: RefCell<Option<Rc<CompositionCommitBatch>>>,
275    /// Whether a batch has ever been asked for: until then no completion
276    /// is waited for, and disposing an object need not release any.
277    used: Cell<bool>,
278}
279
280impl CompositorBatches {
281    pub(crate) fn new() -> CompositorBatches {
282        CompositorBatches::default()
283    }
284
285    fn frame(&self, dispatcher: &Arc<Dispatcher>) -> Arc<BatchShared> {
286        self.frame
287            .borrow_mut()
288            .get_or_insert_with(|| BatchShared::new(dispatcher.clone(), CompositionBatchTypes::NONE, false))
289            .clone()
290    }
291
292    /// Whether a scoped or commit batch has ever been asked for.
293    pub(crate) fn used(&self) -> bool {
294        self.used.get()
295    }
296
297    /// Ends the batch of the frame that is being committed; the next
298    /// animation or `GetCommitBatch` starts the batch of the next frame.
299    pub(crate) fn on_commit(&self) {
300        let frame = self.frame.borrow_mut().take();
301        let commit_batch = self.commit_batch.borrow_mut().take();
302        if let Some(frame) = frame {
303            frame.end();
304        }
305        drop(commit_batch);
306    }
307}
308
309/// An explicitly created group of active animations or effects (WinUI
310/// `CompositionScopedBatch`).
311///
312/// Every key frame animation started while the batch is active (created and
313/// neither suspended nor ended) belongs to it, also the animations of a
314/// group and implicit animations. Once [`end`](Self::end) has been called
315/// and all its animations are over, the batch raises
316/// [`completed`](Self::completed) on the thread of its compositor.
317///
318/// Several batches may be active at once; an animation belongs to each of
319/// them.
320pub struct CompositionScopedBatch {
321    object: CompositionObject,
322    shared: Arc<BatchShared>,
323    is_suspended: Cell<bool>,
324    is_ended: Cell<bool>,
325    completed: Rc<CompletedHandlers>,
326}
327
328impl CompositionScopedBatch {
329    /// The associated compositor.
330    pub fn compositor(&self) -> &Rc<Compositor> {
331        self.object.compositor()
332    }
333
334    /// Whether the batch is open for animations to be aggregated: false
335    /// once it is suspended or ended.
336    pub fn is_active(&self) -> bool {
337        !self.is_suspended.get() && !self.is_ended.get()
338    }
339
340    /// Whether the batch has been closed and can no longer accept changes.
341    pub fn is_ended(&self) -> bool {
342        self.is_ended.get()
343    }
344
345    /// Closes the batch. Once closed it cannot be suspended or resumed
346    /// again. `Completed` is raised once all the animations of the batch are
347    /// over, right away (asynchronously) when there are none. Ending a batch
348    /// again does nothing.
349    pub fn end(&self) {
350        if self.is_ended.get() {
351            return;
352        }
353        self.is_ended.set(true);
354        let this: *const CompositionScopedBatch = self;
355        self.compositor()
356            .animation_batches()
357            .scoped
358            .borrow_mut()
359            .retain(|batch| batch.strong_count() > 0 && !std::ptr::eq(batch.as_ptr(), this));
360        register_pending_completion(self.shared.id, self.completed.clone());
361        self.shared.end();
362    }
363
364    /// Suspends aggregating animations in the batch until
365    /// [`resume`](Self::resume). Panics when the batch is ended (WinUI
366    /// throws).
367    pub fn suspend(&self) {
368        if self.is_ended.get() {
369            panic!("The batch has been ended and cannot be suspended");
370        }
371        self.is_suspended.set(true);
372    }
373
374    /// Resumes aggregating animations in the batch. Panics when the batch
375    /// is ended (WinUI throws).
376    pub fn resume(&self) {
377        if self.is_ended.get() {
378            panic!("The batch has been ended and cannot be resumed");
379        }
380        self.is_suspended.set(false);
381    }
382
383    /// Raised once all animations of the ended batch have completed.
384    /// Disposing the returned handle unsubscribes.
385    // Deviation (winui-composition.md): the handler gets the arguments only,
386    // not the sender, as the other events of FerroUI; the batch need not be
387    // kept alive by the application to raise the event.
388    pub fn completed(&self, handler: impl Fn(&CompositionBatchCompletedEventArgs) + 'static) -> Rc<dyn IDisposable> {
389        subscribe(&self.completed, handler)
390    }
391}
392
393impl AsCompositionObject for CompositionScopedBatch {
394    fn as_composition_object(&self) -> &CompositionObject {
395        &self.object
396    }
397
398    fn composition_type_name(&self) -> &'static str {
399        "CompositionScopedBatch"
400    }
401}
402
403/// The group of the animations started in the current frame (WinUI
404/// `CompositionCommitBatch`): it is ended when the compositor commits the
405/// frame and raises [`completed`](Self::completed) once the animations
406/// started in the frame are over.
407// Deviation (winui-composition.md): there is one commit batch per frame;
408// asking for it again with other types returns the same batch, which then
409// waits for the union of the types.
410pub struct CompositionCommitBatch {
411    object: CompositionObject,
412    shared: Arc<BatchShared>,
413    completed: Rc<CompletedHandlers>,
414}
415
416impl CompositionCommitBatch {
417    /// The associated compositor.
418    pub fn compositor(&self) -> &Rc<Compositor> {
419        self.object.compositor()
420    }
421
422    /// Whether the batch can still aggregate animations: until the frame is
423    /// committed.
424    pub fn is_active(&self) -> bool {
425        !self.shared.is_ended()
426    }
427
428    /// Whether the frame of the batch has been committed.
429    pub fn is_ended(&self) -> bool {
430        self.shared.is_ended()
431    }
432
433    /// Raised once all animations of the batch have completed. Disposing
434    /// the returned handle unsubscribes.
435    pub fn completed(&self, handler: impl Fn(&CompositionBatchCompletedEventArgs) + 'static) -> Rc<dyn IDisposable> {
436        subscribe(&self.completed, handler)
437    }
438}
439
440impl AsCompositionObject for CompositionCommitBatch {
441    fn as_composition_object(&self) -> &CompositionObject {
442        &self.object
443    }
444
445    fn composition_type_name(&self) -> &'static str {
446        "CompositionCommitBatch"
447    }
448}
449
450impl Compositor {
451    /// Creates a scoped batch that aggregates the animations of the given
452    /// types started from now on (WinUI `CreateScopedBatch`).
453    pub fn create_scoped_batch(&self, batch_type: CompositionBatchTypes) -> Rc<CompositionScopedBatch> {
454        let batch = Rc::new(CompositionScopedBatch {
455            object: CompositionObject::new(&self.this_handle(), None),
456            shared: BatchShared::new(self.dispatcher().clone(), batch_type, true),
457            is_suspended: Cell::new(false),
458            is_ended: Cell::new(false),
459            completed: Rc::new(HandlerList::new()),
460        });
461        self.animation_batches().used.set(true);
462        let mut scoped = self.animation_batches().scoped.borrow_mut();
463        scoped.retain(|batch| batch.strong_count() > 0);
464        scoped.push(Rc::downgrade(&batch));
465        batch
466    }
467
468    /// The batch of the animations started in the current frame, which the
469    /// next commit ends (WinUI `GetCommitBatch`). The animations started in
470    /// the frame before the call belong to it too. A commit is requested, so
471    /// the batch ends even if nothing else changes.
472    pub fn get_commit_batch(&self, batch_type: CompositionBatchTypes) -> Rc<CompositionCommitBatch> {
473        let batches = self.animation_batches();
474        batches.used.set(true);
475        let existing = batches.commit_batch.borrow().clone();
476        if let Some(existing) = existing {
477            existing.shared.observe(batch_type);
478            return existing;
479        }
480        let shared = batches.frame(self.dispatcher());
481        shared.observe(batch_type);
482        let batch = Rc::new(CompositionCommitBatch {
483            object: CompositionObject::new(&self.this_handle(), None),
484            shared: shared.clone(),
485            completed: Rc::new(HandlerList::new()),
486        });
487        register_pending_completion(shared.id, batch.completed.clone());
488        *batches.commit_batch.borrow_mut() = Some(batch.clone());
489        self.request_commit_async();
490        batch
491    }
492
493    /// Creates a property set (WinUI `CreatePropertySet`).
494    pub fn create_property_set(&self) -> Rc<CompositionPropertySet> {
495        CompositionPropertySet::new(&self.this_handle())
496    }
497
498    /// The completion guards of an animation started now: one for each
499    /// active scoped batch and one for the batch of the current frame.
500    /// `infinite` is set for an animation that does not end by itself.
501    ///
502    /// Key frame animations take them when their instance is created.
503    // Deviation (winui-composition.md): expression animations belong to no
504    // batch, also not to a batch of infinite animations.
505    pub(crate) fn acquire_animation_completion(&self, infinite: bool) -> AnimationCompletion {
506        let batches = self.animation_batches();
507        let mut guards = Vec::new();
508        let scoped: Vec<Rc<CompositionScopedBatch>> =
509            batches.scoped.borrow().iter().filter_map(Weak::upgrade).collect();
510        for batch in scoped {
511            if batch.is_active() {
512                guards.push(batch.shared.acquire(infinite));
513            }
514        }
515        guards.push(batches.frame(self.dispatcher()).acquire(infinite));
516        AnimationCompletion { guards }
517    }
518}