Skip to main content

ferroui_base/rendering/composition/
compositor.rs

1use super::server::{BatchQueue, CompositorClock, ServerCompositionVisual, ServerCompositor, ServerObjectId};
2use super::transport::{
3    BatchMarker, BatchObject, BatchStreamWriter, CommittedBatch, CompositionBatch, ServerJob, ServerObjectFactory, ServerObjectJob,
4};
5use super::{CompositionOptions, CompositionVisual, ICompositorSerializable};
6use crate::animation::easings::{IEasing, SplineEasing};
7use crate::animation::KeySpline;
8use crate::media::imaging::Bitmap;
9use crate::platform::IPlatformGraphics;
10use crate::reactive::{Disposable, IDisposable};
11use crate::rendering::composition::server::IServerObject;
12use crate::rendering::{IRenderLoop, IRenderLoopTask};
13use crate::threading::{Dispatcher, DispatcherPriority};
14use crate::utilities::HandlerList;
15use crate::{FerroLocator, LocatorExtensions};
16use std::cell::{Cell, RefCell};
17use std::collections::{HashMap, HashSet, VecDeque};
18use std::rc::{Rc, Weak};
19use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
20use std::sync::{Arc, Mutex};
21use std::time::Duration;
22
23/// Receives the request to commit a compositor's pending changes; the media
24/// context implements it and schedules the commit with the next frame.
25pub trait ICompositorScheduler {
26    fn commit_requested(&self, compositor: &Rc<Compositor>);
27}
28
29static NEXT_COMPOSITOR_KEY: AtomicU64 = AtomicU64::new(1);
30
31crate::ferro_thread_local! {
32    /// The compositors of this thread by key: lets `Send` callbacks that
33    /// come back to the thread find their compositor.
34    static COMPOSITORS: RefCell<HashMap<u64, Weak<Compositor>>> = RefCell::new(HashMap::new());
35}
36
37fn find_compositor(key: u64) -> Option<Rc<Compositor>> {
38    COMPOSITORS.with(|compositors| compositors.borrow().get(&key).and_then(Weak::upgrade))
39}
40
41/// The compositor: manages the composition objects of the UI thread and
42/// commits their changes to the server compositor in batches.
43pub struct Compositor {
44    this: Weak<Compositor>,
45    key: u64,
46    render_loop: Arc<dyn IRenderLoop>,
47    loop_task: Arc<dyn IRenderLoopTask>,
48    use_ui_thread_for_synchronous_commits: bool,
49    /// The server compositor, behind the compositor lock.
50    server: Arc<super::server::LockedServerCompositor>,
51    /// Whether the render loop renders on the thread that ticks it (the
52    /// render-thread mode); otherwise a tick is marshalled to the thread of
53    /// this compositor (the dispatcher-thread mode).
54    render_thread: bool,
55    /// The loop task of the render-thread mode when the server compositor is
56    /// confined to the render thread (see
57    /// [`is_confined_to_render_thread`](Self::is_confined_to_render_thread)):
58    /// the compositor asks it for the release of the server graph.
59    confined_loop_task: Option<Arc<RenderThreadLoopTask>>,
60    /// Whether a job of the render thread is on its way to fill the cache
61    /// of the features of the render interface.
62    /// The readback of the server: what the two threads share besides the
63    /// queue of batches.
64    readback: Arc<super::server::ReadbackIndices>,
65    batches: Arc<BatchQueue>,
66    clock: CompositorClock,
67    next_commit: RefCell<Option<Arc<CompositionBatch>>>,
68    object_serialization_queue: RefCell<VecDeque<Rc<dyn ICompositorSerializable>>>,
69    object_serialization_hash_set: RefCell<HashSet<*const ()>>,
70    invoke_before_commit_write: RefCell<VecDeque<Box<dyn FnOnce()>>>,
71    invoke_before_commit_read: RefCell<VecDeque<Box<dyn FnOnce()>>>,
72    dispose_on_next_batch: RefCell<Vec<PendingDisposal>>,
73    pending_creations: RefCell<Vec<(ServerObjectId, ServerObjectFactory)>>,
74    next_server_object_id: Cell<u32>,
75    /// The ids that can be handed out again: each was released by a batch
76    /// that is committed, so the server drops its object before it reads a
77    /// creation under the same id, which can only come with a later batch.
78    free_server_object_ids: RefCell<Vec<u32>>,
79    /// By id, whether the id is handed out: an id is returned to the free
80    /// list once, however often its release is asked for.
81    server_object_id_in_use: RefCell<Vec<bool>>,
82    pending_batch: Arc<Mutex<Option<Arc<CompositionBatch>>>>,
83    pending_server_compositor_jobs: RefCell<Vec<PendingServerJob>>,
84    pending_server_compositor_post_target_jobs: RefCell<Vec<PendingServerJob>>,
85    scheduler: Weak<dyn ICompositorScheduler>,
86    dispatcher: Arc<Dispatcher>,
87    after_commit: HandlerList<dyn Fn()>,
88    default_easing: Rc<dyn IEasing>,
89}
90
91/// The render loop task of a compositor. The loop may tick on any thread;
92/// the server compositor is confined to the thread of its compositor, so a
93/// tick from elsewhere is marshalled there.
94struct ServerCompositorLoopTask {
95    key: u64,
96    dispatcher: Arc<Dispatcher>,
97    marshalled: Arc<AtomicBool>,
98    last_result: Arc<AtomicBool>,
99}
100
101impl IRenderLoopTask for ServerCompositorLoopTask {
102    fn render(&self) -> bool {
103        if let Some(compositor) = find_compositor(self.key) {
104            let result = compositor.server().render();
105            self.last_result.store(result, Ordering::SeqCst);
106            return result;
107        }
108        if !self.marshalled.swap(true, Ordering::SeqCst) {
109            let key = self.key;
110            let marshalled = self.marshalled.clone();
111            let last_result = self.last_result.clone();
112            self.dispatcher.post(
113                move || {
114                    marshalled.store(false, Ordering::SeqCst);
115                    if let Some(compositor) = find_compositor(key) {
116                        last_result.store(compositor.server().render(), Ordering::SeqCst);
117                    }
118                },
119                DispatcherPriority::RENDER,
120            );
121        }
122        self.last_result.load(Ordering::SeqCst)
123    }
124}
125
126/// The clock of a compositor whose server runs on the render thread: read
127/// by both threads.
128pub type SharedCompositorClock = Arc<dyn Fn() -> Duration + Send + Sync>;
129
130/// The render loop task of a compositor in the render-thread mode: the
131/// thread that ticks renders, under the compositor lock.
132struct RenderThreadLoopTask {
133    this: std::sync::Weak<RenderThreadLoopTask>,
134    server: Arc<super::server::LockedServerCompositor>,
135    /// Whether the thread that ticks is the only one that renders: the
136    /// server graph is then confined to it, and released by it.
137    confined: bool,
138    /// Set by the compositor when it is dropped, where `confined`: the next
139    /// tick releases the server graph instead of rendering.
140    release_requested: AtomicBool,
141    removal_posted: AtomicBool,
142    render_loop: std::sync::Weak<dyn IRenderLoop>,
143    dispatcher: Arc<Dispatcher>,
144}
145
146impl RenderThreadLoopTask {
147    /// Asks the render thread to release the server graph with its next
148    /// tick. From then on no frame is rendered.
149    fn request_release(&self) {
150        self.release_requested.store(true, Ordering::SeqCst);
151    }
152
153    /// Takes the task out of the render loop once the graph is released. A
154    /// task is added and removed by the thread of the compositor, so the
155    /// removal is posted there.
156    fn remove_from_loop(&self) {
157        if self.removal_posted.swap(true, Ordering::SeqCst) {
158            return;
159        }
160        let Some(this) = self.this.upgrade() else { return };
161        let render_loop = self.render_loop.clone();
162        self.dispatcher.post(
163            move || {
164                if let Some(render_loop) = render_loop.upgrade() {
165                    let task: Arc<dyn IRenderLoopTask> = this;
166                    render_loop.remove(&task);
167                }
168            },
169            DispatcherPriority::SEND,
170        );
171    }
172}
173
174impl IRenderLoopTask for RenderThreadLoopTask {
175    fn render(&self) -> bool {
176        if self.confined {
177            self.server.confine_to_current_thread();
178        }
179        // The request is read inside the lock: the compositor sets it before
180        // it enters the lock to take its handles back, so a frame either
181        // ends before that or does not start.
182        let rendered = self
183            .server
184            .try_with(|server| (!self.release_requested.load(Ordering::SeqCst)).then(|| server.render()));
185        match rendered {
186            Some(Some(wants_next_tick)) => wants_next_tick,
187            Some(None) => {
188                // The last job of the render thread: the graph, and with it
189                // every render target and the graphics context, is dropped
190                // here. What it drew with is released first, while its own
191                // graphics context is current: a thread that renders several
192                // compositors has several contexts, and between two frames
193                // any of them, or none, is the current one.
194                self.server.try_with(|server| server.release_gpu_resources());
195                self.server.release();
196                self.remove_from_loop();
197                false
198            }
199            // Nothing to render once the compositor is gone.
200            None => false,
201        }
202    }
203}
204
205impl Compositor {
206    /// Creates a compositor in the render-thread mode: the thread that ticks
207    /// `render_loop` renders the frames.
208    ///
209    /// The server compositor is confined to the compositor lock, not to a
210    /// thread: with `use_ui_thread_for_synchronous_commits` the thread of
211    /// this compositor renders too, at the synchronous points (a resize, the
212    /// first show), as the reference does on platforms that ask for it.
213    ///
214    /// Without it, and with a loop that runs in the background, the server
215    /// compositor is also confined to the thread that ticks (see
216    /// [`is_confined_to_render_thread`](Self::is_confined_to_render_thread)).
217    /// The loop says where it runs when the compositor is created.
218    ///
219    /// `gpu` is shared with the caller, who may keep its handle: the server
220    /// side holds one of its own and the thread that renders asks it for
221    /// the graphics context.
222    pub fn with_render_thread(
223        render_loop: Arc<dyn IRenderLoop>,
224        gpu: Option<Arc<dyn IPlatformGraphics>>,
225        use_ui_thread_for_synchronous_commits: bool,
226        scheduler: &Rc<dyn ICompositorScheduler>,
227        dispatcher: Arc<Dispatcher>,
228        options: Option<CompositionOptions>,
229        clock: Option<SharedCompositorClock>,
230    ) -> Rc<Compositor> {
231        let options = options
232            .or_else(|| FerroLocator::current().get_service::<CompositionOptions>().map(|o| *o))
233            .unwrap_or_default();
234        // The clock is read by both threads. Each side has a handle of its
235        // own to it: the server graph may be released by the render thread.
236        let clock = clock.unwrap_or_else(|| {
237            let origin = std::time::Instant::now();
238            Arc::new(move || origin.elapsed())
239        });
240        let server_clock: CompositorClock = {
241            let clock = clock.clone();
242            Rc::new(move || clock())
243        };
244        let clock: CompositorClock = Rc::new(move || clock());
245        let batches = Arc::new(BatchQueue::default());
246        let readback = Arc::new(super::server::ReadbackIndices::new());
247        let server = Arc::new(super::server::LockedServerCompositor::new(ServerCompositor::new(
248            gpu,
249            options,
250            batches.clone(),
251            readback.clone(),
252            server_clock,
253        )));
254        let key = NEXT_COMPOSITOR_KEY.fetch_add(1, Ordering::SeqCst);
255        // The condition of the reference for a thread of the compositor that
256        // never renders (`UseUiThreadForSynchronousCommits: false`,
257        // `Loop.RunsInBackground: true`).
258        let confined = !use_ui_thread_for_synchronous_commits && render_loop.runs_in_background();
259        let task = Arc::new_cyclic(|this| RenderThreadLoopTask {
260            this: this.clone(),
261            server: server.clone(),
262            confined,
263            release_requested: AtomicBool::new(false),
264            removal_posted: AtomicBool::new(false),
265            render_loop: Arc::downgrade(&render_loop),
266            dispatcher: dispatcher.clone(),
267        });
268        let loop_task: Arc<dyn IRenderLoopTask> = task.clone();
269        Self::build(
270            render_loop,
271            loop_task,
272            confined.then_some(task),
273            use_ui_thread_for_synchronous_commits,
274            server,
275            true,
276            batches,
277            readback,
278            clock,
279            scheduler,
280            dispatcher,
281            key,
282        )
283    }
284
285    /// Creates a compositor that renders on the render loop registered in
286    /// the service locator and commits through the media context.
287    ///
288    /// The threads follow the render loop, as in the reference: with a loop
289    /// that runs in the background (the thread of the platform's render
290    /// timer) that thread renders the frames, under the compositor lock (the
291    /// render-thread mode); with a loop of the UI thread the UI thread
292    /// renders. `FERROUI_RENDER_THREAD=0` in the environment keeps the
293    /// rendering on the UI thread with a background loop too (each tick is
294    /// marshalled to it): a way back while the mode is young.
295    pub fn new(gpu: Option<Arc<dyn IPlatformGraphics>>, use_ui_thread_for_synchronous_commits: bool) -> Rc<Compositor> {
296        let render_loop = FerroLocator::current().get_required_service::<Arc<dyn IRenderLoop>>();
297        let scheduler: Rc<dyn ICompositorScheduler> = crate::media::MediaContext::instance().scheduler();
298        let render_thread_disabled = std::env::var("FERROUI_RENDER_THREAD").is_ok_and(|value| value == "0");
299        if render_loop.runs_in_background() && !render_thread_disabled {
300            return Self::with_render_thread(
301                (*render_loop).clone(),
302                gpu,
303                use_ui_thread_for_synchronous_commits,
304                &scheduler,
305                Dispatcher::ui_thread(),
306                None,
307                None,
308            );
309        }
310        Self::with_scheduler(
311            (*render_loop).clone(),
312            gpu,
313            use_ui_thread_for_synchronous_commits,
314            &scheduler,
315            Dispatcher::ui_thread(),
316            None,
317            None,
318        )
319    }
320
321    /// Creates a compositor with every collaborator given explicitly.
322    ///
323    /// `options` defaults to the options registered in the service locator;
324    /// `clock` defaults to the dispatcher's clock.
325    pub fn with_scheduler(
326        render_loop: Arc<dyn IRenderLoop>,
327        gpu: Option<Arc<dyn IPlatformGraphics>>,
328        use_ui_thread_for_synchronous_commits: bool,
329        scheduler: &Rc<dyn ICompositorScheduler>,
330        dispatcher: Arc<Dispatcher>,
331        options: Option<CompositionOptions>,
332        clock: Option<CompositorClock>,
333    ) -> Rc<Compositor> {
334        let options = options
335            .or_else(|| FerroLocator::current().get_service::<CompositionOptions>().map(|o| *o))
336            .unwrap_or_default();
337        let clock = clock.unwrap_or_else(|| {
338            let platform_impl = dispatcher.platform_impl();
339            let origin = platform_impl.now();
340            Rc::new(move || Duration::from_millis((platform_impl.now() - origin).max(0) as u64))
341        });
342        let batches = Arc::new(BatchQueue::default());
343        let readback = Arc::new(super::server::ReadbackIndices::new());
344        let server = Arc::new(super::server::LockedServerCompositor::new(ServerCompositor::new(
345            gpu,
346            options,
347            batches.clone(),
348            readback.clone(),
349            clock.clone(),
350        )));
351        let key = NEXT_COMPOSITOR_KEY.fetch_add(1, Ordering::SeqCst);
352        let loop_task: Arc<dyn IRenderLoopTask> = Arc::new(ServerCompositorLoopTask {
353            key,
354            dispatcher: dispatcher.clone(),
355            marshalled: Arc::new(AtomicBool::new(false)),
356            last_result: Arc::new(AtomicBool::new(true)),
357        });
358        Self::build(
359            render_loop,
360            loop_task,
361            None,
362            use_ui_thread_for_synchronous_commits,
363            server,
364            false,
365            batches,
366            readback,
367            clock,
368            scheduler,
369            dispatcher,
370            key,
371        )
372    }
373
374    #[allow(clippy::too_many_arguments)]
375    fn build(
376        render_loop: Arc<dyn IRenderLoop>,
377        loop_task: Arc<dyn IRenderLoopTask>,
378        confined_loop_task: Option<Arc<RenderThreadLoopTask>>,
379        use_ui_thread_for_synchronous_commits: bool,
380        server: Arc<super::server::LockedServerCompositor>,
381        render_thread: bool,
382        batches: Arc<BatchQueue>,
383        readback: Arc<super::server::ReadbackIndices>,
384        clock: CompositorClock,
385        scheduler: &Rc<dyn ICompositorScheduler>,
386        dispatcher: Arc<Dispatcher>,
387        key: u64,
388    ) -> Rc<Compositor> {
389        let compositor = Rc::new_cyclic(|this| Compositor {
390            this: this.clone(),
391            key,
392            render_loop: render_loop.clone(),
393            loop_task: loop_task.clone(),
394            use_ui_thread_for_synchronous_commits,
395            readback,
396            server,
397            render_thread,
398            confined_loop_task,
399            batches,
400            clock,
401            next_commit: RefCell::new(None),
402            object_serialization_queue: RefCell::new(VecDeque::new()),
403            object_serialization_hash_set: RefCell::new(HashSet::new()),
404            invoke_before_commit_write: RefCell::new(VecDeque::new()),
405            invoke_before_commit_read: RefCell::new(VecDeque::new()),
406            dispose_on_next_batch: RefCell::new(Vec::new()),
407            pending_creations: RefCell::new(Vec::new()),
408            next_server_object_id: Cell::new(0),
409            free_server_object_ids: RefCell::new(Vec::new()),
410            server_object_id_in_use: RefCell::new(Vec::new()),
411            pending_batch: Arc::new(Mutex::new(None)),
412            pending_server_compositor_jobs: RefCell::new(Vec::new()),
413            pending_server_compositor_post_target_jobs: RefCell::new(Vec::new()),
414            scheduler: Rc::downgrade(scheduler),
415            dispatcher,
416            after_commit: HandlerList::new(),
417            default_easing: Rc::new(SplineEasing::with_key_spline(KeySpline::with_points(0.25, 0.1, 0.25, 1.0))),
418        });
419        COMPOSITORS.with(|compositors| {
420            let mut compositors = compositors.borrow_mut();
421            compositors.retain(|_, c| c.strong_count() > 0);
422            compositors.insert(key, Rc::downgrade(&compositor));
423        });
424        render_loop.add(loop_task);
425        compositor
426    }
427
428    /// The easing of the key frames that are inserted without one.
429    pub(crate) fn default_easing(&self) -> Rc<dyn IEasing> {
430        self.default_easing.clone()
431    }
432
433    /// The render loop the server compositor is driven by.
434    pub fn render_loop(&self) -> &Arc<dyn IRenderLoop> {
435        &self.render_loop
436    }
437
438    pub fn use_ui_thread_for_synchronous_commits(&self) -> bool {
439        self.use_ui_thread_for_synchronous_commits
440    }
441
442    /// The server compositor. It belongs to the render thread: UI-thread
443    /// code may only use it to drive a frame when the render loop runs on
444    /// the UI thread, and in tests.
445    pub fn server(&self) -> &Rc<ServerCompositor> {
446        assert!(
447            !self.render_thread,
448            "in the render-thread mode the server compositor is reached under its lock: with_server"
449        );
450        self.server.same_thread()
451    }
452
453    /// Runs `f` with the server compositor under the compositor lock: the
454    /// way in from this thread in either mode. What `f` gets must not leave
455    /// it.
456    pub fn with_server<R>(&self, f: impl FnOnce(&Rc<ServerCompositor>) -> R) -> R {
457        self.server.with(f)
458    }
459
460    /// The server compositor behind its lock, as a handle that may cross to
461    /// the thread that renders: for work of that thread that cannot be a job
462    /// of a batch, because it has to run whether or not this compositor
463    /// commits again and whether or not its graphics are ready (the release
464    /// of a surface that is closed). The work enters the graph with
465    /// [`try_with`](super::server::LockedServerCompositor::try_with), which
466    /// answers `None` once the graph is released.
467    ///
468    /// Not from upstream, where such work captures the server compositor
469    /// itself.
470    pub fn locked_server(&self) -> Arc<super::server::LockedServerCompositor> {
471        self.server.clone()
472    }
473
474    /// Binds `value` to the compositor lock: an object of the server side
475    /// that this thread asks for and jobs of the render thread use (see
476    /// [`LockBound`](super::server::LockBound)). `server` is what
477    /// [`with_server`](Self::with_server) or a job hands out.
478    pub(crate) fn bind_to_lock<T>(&self, server: &ServerCompositor, value: T) -> super::server::LockBound<T> {
479        super::server::LockBound::new(&self.server, server, value)
480    }
481
482    /// Renders a frame on this thread, under the compositor lock: what the
483    /// synchronous points do (`Server.Render` upstream).
484    ///
485    /// # Panics
486    ///
487    /// When the compositor is confined to its render thread: this thread
488    /// waits for a frame there, it never renders one.
489    pub fn render_on_this_thread(&self) -> bool {
490        assert!(
491            !self.is_confined_to_render_thread(),
492            "a compositor that is confined to its render thread is not rendered by the thread of the compositor"
493        );
494        self.server.with(|server| server.render())
495    }
496
497    /// Whether the thread that ticks the render loop renders the frames,
498    /// not the thread of this compositor.
499    pub fn renders_on_render_thread(&self) -> bool {
500        self.render_thread
501    }
502
503    /// Whether the thread that ticks the render loop is the only one that
504    /// renders: the render-thread mode without
505    /// `use_ui_thread_for_synchronous_commits`, on a loop that runs in the
506    /// background (the condition under which the reference waits for the
507    /// render thread at the synchronous points).
508    ///
509    /// The render targets, the graphics context and everything else that
510    /// belongs to the thread that draws are then created, used and dropped
511    /// by the render thread alone. This thread still enters the compositor
512    /// lock ([`with_server`](Self::with_server)), for what does not render:
513    /// it commits batches, posts jobs and waits for them.
514    pub fn is_confined_to_render_thread(&self) -> bool {
515        self.confined_loop_task.is_some()
516    }
517
518    /// The readback indices the server writes and this side reads.
519    pub fn readback(&self) -> &Arc<super::server::ReadbackIndices> {
520        &self.readback
521    }
522
523    /// The dispatcher of the thread the compositor belongs to.
524    pub fn dispatcher(&self) -> &Arc<Dispatcher> {
525        &self.dispatcher
526    }
527
528    /// The time elapsed on the compositor clock.
529    pub fn clock_elapsed(&self) -> Duration {
530        (self.clock)()
531    }
532
533    /// Subscribes to the notification raised after each commit.
534    pub fn after_commit(&self, handler: impl Fn() + 'static) -> Rc<dyn IDisposable> {
535        let token = self.after_commit.add(Rc::new(handler));
536        let weak = self.this.clone();
537        Disposable::create(move || {
538            if let Some(this) = weak.upgrade() {
539                this.after_commit.remove(token);
540            }
541        })
542    }
543
544    pub(crate) fn this_handle(&self) -> Rc<Compositor> {
545        self.this()
546    }
547
548    fn this(&self) -> Rc<Compositor> {
549        self.this.upgrade().expect("the compositor is alive while it is used")
550    }
551
552    fn trigger_commit_requested(&self) {
553        if let Some(scheduler) = self.scheduler.upgrade() {
554            scheduler.commit_requested(&self.this());
555        }
556    }
557
558    /// Requests the pending changes to be committed and returns the batch
559    /// they will be part of.
560    pub fn request_composition_batch_commit_async(&self) -> Arc<CompositionBatch> {
561        self.dispatcher.verify_access();
562        if let Some(next_commit) = self.next_commit.borrow().clone() {
563            return next_commit;
564        }
565        let next_commit = CompositionBatch::new();
566        *self.next_commit.borrow_mut() = Some(next_commit.clone());
567        let pending = self.pending_batch.lock().unwrap_or_else(|e| e.into_inner()).clone();
568        match pending {
569            Some(pending) => {
570                // The previous batch has not been processed yet: ask for
571                // the commit once it has been.
572                let key = self.key;
573                let dispatcher = self.dispatcher.clone();
574                pending.processed().on_completed(move || {
575                    dispatcher.post(
576                        move || {
577                            if let Some(compositor) = find_compositor(key) {
578                                compositor.trigger_commit_requested();
579                            }
580                        },
581                        DispatcherPriority::SEND,
582                    );
583                });
584            }
585            None => self.trigger_commit_requested(),
586        }
587        next_commit
588    }
589
590    /// Requests the pending changes to be committed.
591    pub fn request_commit_async(&self) -> Arc<CompositionBatch> {
592        self.request_composition_batch_commit_async()
593    }
594
595    /// Serializes the pending changes into a batch and hands it to the
596    /// server compositor.
597    pub fn commit(&self) -> Arc<CompositionBatch> {
598        let result = self.commit_core();
599        if !self.invoke_before_commit_write.borrow().is_empty() {
600            self.request_commit_async();
601        }
602        if !self.after_commit.is_empty() {
603            for (_, handler) in self.after_commit.snapshot().iter() {
604                handler();
605            }
606        }
607        result
608    }
609
610    fn commit_core(&self) -> Arc<CompositionBatch> {
611        self.dispatcher.verify_access();
612
613        let commit = self.next_commit.borrow_mut().get_or_insert_with(CompositionBatch::new).clone();
614
615        std::mem::swap(
616            &mut *self.invoke_before_commit_read.borrow_mut(),
617            &mut *self.invoke_before_commit_write.borrow_mut(),
618        );
619        loop {
620            let Some(action) = self.invoke_before_commit_read.borrow_mut().pop_front() else { break };
621            action();
622        }
623
624        let mut changes = self.batches.rent_data();
625        let disposed: Vec<PendingDisposal>;
626        {
627            let mut writer = BatchStreamWriter::new(&mut changes);
628
629            // Objects serialize references to server objects, so the objects
630            // created since the last commit come first.
631            let mut objects_written = false;
632            // The set of queued objects knows an object by its address, so
633            // an object stays alive until the set is cleared: an object
634            // created while another one is serialized can then never get the
635            // address of one that was serialized (and released) before it,
636            // and be taken for queued.
637            let mut serialized: Vec<Rc<dyn ICompositorSerializable>> = Vec::new();
638            loop {
639                self.write_pending_creations(&mut writer);
640                let Some(object) = self.object_serialization_queue.borrow_mut().pop_front() else { break };
641                if let Some(server_object) = object.try_get_server(self) {
642                    // Server objects the object creates for its changes
643                    // must exist before the changes are read.
644                    object.prepare_serialization(self);
645                    self.write_pending_creations(&mut writer);
646                    writer.write_server_object(Some(server_object));
647                    object.serialize_changes(self, &mut writer);
648                    if cfg!(debug_assertions) {
649                        writer.write(super::server::OBJECT_END_MAGIC);
650                        writer.write_marker(BatchMarker::ObjectEnd);
651                    }
652                    objects_written = true;
653                }
654                serialized.push(object);
655            }
656            let _ = objects_written;
657            self.object_serialization_hash_set.borrow_mut().clear();
658            drop(serialized);
659
660            disposed = std::mem::take(&mut *self.dispose_on_next_batch.borrow_mut());
661            if !disposed.is_empty() {
662                writer.write_marker(BatchMarker::RenderThreadDisposeStart);
663                writer.write(disposed.len() as i32);
664                for entry in &disposed {
665                    writer.write_server_object(Some(entry.id));
666                    writer.write(entry.dispose);
667                    writer.write(entry.release);
668                }
669            }
670
671            Self::serialize_server_jobs(
672                &mut writer,
673                &self.pending_server_compositor_jobs,
674                BatchMarker::RenderThreadJobsStart,
675                BatchMarker::RenderThreadJobsEnd,
676            );
677            Self::serialize_server_jobs(
678                &mut writer,
679                &self.pending_server_compositor_post_target_jobs,
680                BatchMarker::RenderThreadPostTargetJobsStart,
681                BatchMarker::RenderThreadPostTargetJobsEnd,
682            );
683        }
684        // Ids released by this batch can be reused by objects created for a
685        // later one.
686        {
687            let mut in_use = self.server_object_id_in_use.borrow_mut();
688            let mut free = self.free_server_object_ids.borrow_mut();
689            for entry in disposed.iter().filter(|entry| entry.release) {
690                if in_use.get_mut(entry.id.index()).is_some_and(|in_use| std::mem::replace(in_use, false)) {
691                    free.push(entry.id.0);
692                }
693            }
694        }
695
696        self.enqueue_batch(CommittedBatch { batch: commit.clone(), changes, committed_at: self.clock_elapsed() });
697
698        *self.pending_batch.lock().unwrap_or_else(|e| e.into_inner()) = Some(commit.clone());
699        let pending_batch = self.pending_batch.clone();
700        let sequence_id = commit.sequence_id();
701        commit.processed().on_completed(move || {
702            let mut pending = pending_batch.lock().unwrap_or_else(|e| e.into_inner());
703            if pending.as_ref().is_some_and(|p| p.sequence_id() == sequence_id) {
704                *pending = None;
705            }
706        });
707        *self.next_commit.borrow_mut() = None;
708
709        commit
710    }
711
712    fn write_pending_creations(&self, writer: &mut BatchStreamWriter<'_>) {
713        let creations = std::mem::take(&mut *self.pending_creations.borrow_mut());
714        if creations.is_empty() {
715            return;
716        }
717        writer.write_marker(BatchMarker::CreateStart);
718        writer.write(creations.len() as i32);
719        for (id, factory) in creations {
720            writer.write_server_object(Some(id));
721            writer.write_object(BatchObject::Create(factory));
722        }
723    }
724
725    fn serialize_server_jobs(
726        writer: &mut BatchStreamWriter<'_>,
727        list: &RefCell<Vec<PendingServerJob>>,
728        start_marker: BatchMarker,
729        end_marker: BatchMarker,
730    ) {
731        let jobs = std::mem::take(&mut *list.borrow_mut());
732        if !jobs.is_empty() {
733            writer.write_marker(start_marker);
734            for job in jobs {
735                match job {
736                    PendingServerJob::Job(job) => writer.write_object(BatchObject::Job(job)),
737                    PendingServerJob::ObjectJob(target, job) => {
738                        writer.write_server_object(Some(target));
739                        writer.write_object(BatchObject::ObjectJob(job));
740                    }
741                }
742            }
743            writer.write_marker(end_marker);
744        }
745    }
746
747    fn enqueue_batch(&self, batch: CommittedBatch) {
748        self.batches.enqueue(batch);
749        self.render_loop.wakeup();
750    }
751
752    /// Disposes a server object in a batch of its own, outside of the
753    /// regular commit.
754    ///
755    /// The object is disposed, not released: it stays in the table of the
756    /// server under its id until the UI-thread object that names it is
757    /// dropped, as after an ordinary disposal. An object whose creation has
758    /// not been committed yet is created by the same batch, before it is
759    /// disposed: upstream the server object exists from the moment the
760    /// UI-thread object does, so the disposal always finds it. Left to the
761    /// next commit, the creation would follow the disposal and the object
762    /// would never be disposed.
763    pub(crate) fn oob_dispose(&self, server: ServerObjectId) -> Arc<CompositionBatch> {
764        // A disposal that was queued for the next batch is done here.
765        self.dispose_on_next_batch.borrow_mut().retain_mut(|entry| {
766            if entry.id == server {
767                entry.dispose = false;
768                entry.release
769            } else {
770                true
771            }
772        });
773        let pending_creation = {
774            let mut creations = self.pending_creations.borrow_mut();
775            creations.iter().position(|(id, _)| *id == server).map(|index| creations.remove(index))
776        };
777        let batch = CompositionBatch::new();
778        let mut changes = self.batches.rent_data();
779        {
780            let mut writer = BatchStreamWriter::new(&mut changes);
781            if let Some((id, factory)) = pending_creation {
782                writer.write_marker(BatchMarker::CreateStart);
783                writer.write(1i32);
784                writer.write_server_object(Some(id));
785                writer.write_object(BatchObject::Create(factory));
786            }
787            writer.write_marker(BatchMarker::RenderThreadDisposeStart);
788            writer.write(1i32);
789            writer.write_server_object(Some(server));
790            writer.write(true);
791            writer.write(false);
792        }
793        self.enqueue_batch(CommittedBatch { batch: batch.clone(), changes, committed_at: self.clock_elapsed() });
794        batch
795    }
796
797    /// Allocates the id of a new server object and queues its creation for
798    /// the next batch. `factory` runs on the render thread.
799    pub fn create_server_object(
800        &self,
801        factory: impl FnOnce(&Rc<ServerCompositor>, ServerObjectId) -> Rc<dyn IServerObject> + Send + 'static,
802    ) -> ServerObjectId {
803        self.dispatcher.verify_access();
804        let id = match self.free_server_object_ids.borrow_mut().pop() {
805            Some(id) => ServerObjectId(id),
806            None => {
807                let id = self.next_server_object_id.get();
808                self.next_server_object_id.set(id + 1);
809                self.server_object_id_in_use.borrow_mut().push(false);
810                ServerObjectId(id)
811            }
812        };
813        self.server_object_id_in_use.borrow_mut()[id.index()] = true;
814        self.pending_creations.borrow_mut().push((id, Box::new(factory)));
815        self.request_commit_async();
816        id
817    }
818
819    /// Queues an object whose changes must be written to the next batch.
820    pub fn register_for_serialization(&self, composition_object: Rc<dyn ICompositorSerializable>) {
821        self.dispatcher.verify_access();
822        let key = composition_object.serialization_key();
823        if self.object_serialization_hash_set.borrow_mut().insert(key) {
824            self.object_serialization_queue.borrow_mut().push_back(composition_object);
825        }
826        self.request_commit_async();
827    }
828
829    /// Disposes a server object with the next batch, and releases it: the
830    /// server compositor drops it and its id can be reused.
831    pub fn dispose_on_next_batch(&self, obj: ServerObjectId) {
832        if self.queue_disposal(obj, true, true) {
833            self.request_commit_async();
834        }
835    }
836
837    /// Disposes a server object with the next batch but keeps it, under its
838    /// id, until [`release_with_a_later_batch`](Self::release_with_a_later_batch):
839    /// the disposal of a composition object, whose server object stays
840    /// reachable by the jobs and animations that refer to it while the
841    /// UI-thread object is alive, as upstream the UI-thread object holds its
842    /// server object.
843    pub(crate) fn dispose_and_keep_on_next_batch(&self, obj: ServerObjectId) {
844        if self.queue_disposal(obj, true, false) {
845            self.request_commit_async();
846        }
847    }
848
849    /// Disposes a server object with a later batch, without asking for a
850    /// commit: used when a UI-thread object is dropped, which must not
851    /// schedule work by itself.
852    pub(crate) fn dispose_with_a_later_batch(&self, obj: ServerObjectId) {
853        self.queue_disposal(obj, true, true);
854    }
855
856    /// Releases a server object that was already disposed with a later
857    /// batch, without asking for a commit: used when a disposed UI-thread
858    /// object is dropped.
859    pub(crate) fn release_with_a_later_batch(&self, obj: ServerObjectId) {
860        self.queue_disposal(obj, false, true);
861    }
862
863    /// Adds `obj` to the disposal list of the next batch, or merges the
864    /// request into its entry. Returns whether the list changed.
865    fn queue_disposal(&self, obj: ServerObjectId, dispose: bool, release: bool) -> bool {
866        // An id that was released already names no object: asking again
867        // for its release must not return it to the free list twice.
868        if !self.server_object_id_in_use.borrow().get(obj.index()).copied().unwrap_or(false) {
869            return false;
870        }
871        let Ok(mut list) = self.dispose_on_next_batch.try_borrow_mut() else { return false };
872        match list.iter_mut().find(|entry| entry.id == obj) {
873            Some(entry) => {
874                let changed = (dispose && !entry.dispose) || (release && !entry.release);
875                entry.dispose |= dispose;
876                entry.release |= release;
877                changed
878            }
879            None => {
880                list.push(PendingDisposal { id: obj, dispose, release });
881                true
882            }
883        }
884    }
885
886    /// Enqueues a callback to be run before the next commit.
887    pub fn request_composition_update(&self, action: impl FnOnce() + 'static) {
888        self.dispatcher.verify_access();
889        self.invoke_before_commit_write.borrow_mut().push_back(Box::new(action));
890        self.request_commit_async();
891    }
892
893    /// Posts a job that runs on the render thread with the next batch:
894    /// before the composition targets are rendered, or after them when
895    /// `post_target` is set.
896    pub fn post_server_job(&self, job: impl FnOnce(&ServerCompositor) + Send + 'static, post_target: bool) {
897        self.dispatcher.verify_access();
898        let list = if post_target {
899            &self.pending_server_compositor_post_target_jobs
900        } else {
901            &self.pending_server_compositor_jobs
902        };
903        list.borrow_mut().push(PendingServerJob::Job(Box::new(job)));
904        self.request_commit_async();
905    }
906
907    /// Posts a job that runs on the render thread with the next batch and
908    /// receives the server object `target` (`None` when it does not
909    /// exist). Upstream a job holds the server object itself; this job
910    /// names it, and the object is resolved when the batch is read, so the
911    /// job reaches an object that the same batch disposes.
912    pub fn post_server_object_job(
913        &self,
914        target: ServerObjectId,
915        job: impl FnOnce(&ServerCompositor, Option<Rc<dyn IServerObject>>) + Send + 'static,
916        post_target: bool,
917    ) {
918        self.dispatcher.verify_access();
919        let list = if post_target {
920            &self.pending_server_compositor_post_target_jobs
921        } else {
922            &self.pending_server_compositor_jobs
923        };
924        list.borrow_mut().push(PendingServerJob::ObjectJob(target, Box::new(job)));
925        self.request_commit_async();
926    }
927
928    /// [`invoke_server_job_async`](Self::invoke_server_job_async) with a job
929    /// that receives the server object `target`; see
930    /// [`post_server_object_job`](Self::post_server_object_job).
931    pub fn invoke_server_object_job_async<T: Send + 'static>(
932        &self,
933        target: ServerObjectId,
934        job: impl FnOnce(&ServerCompositor, Option<Rc<dyn IServerObject>>) -> Result<T, ServerJobError>
935            + Send
936            + 'static,
937        post_target: bool,
938    ) -> ServerJobTask<T> {
939        let task = ServerJobTask::new();
940        let completion = task.clone();
941        self.post_server_object_job(
942            target,
943            move |compositor, object| match job(compositor, object) {
944                Ok(result) => completion.set_result(result),
945                Err(error) => completion.set_exception(error),
946            },
947            post_target,
948        );
949        task
950    }
951
952    /// Runs `job` on the render thread with the next batch and returns a
953    /// task that completes with its result (`InvokeServerJobAsync`). A job
954    /// that fails faults the task with its error.
955    pub fn invoke_server_job_async<T: Send + 'static>(
956        &self,
957        job: impl FnOnce(&ServerCompositor) -> Result<T, ServerJobError> + Send + 'static,
958        post_target: bool,
959    ) -> ServerJobTask<T> {
960        let task = ServerJobTask::new();
961        let completion = task.clone();
962        self.post_server_job(
963            move |compositor| match job(compositor) {
964                Ok(result) => completion.set_result(result),
965                Err(error) => completion.set_exception(error),
966            },
967            post_target,
968        );
969        task
970    }
971
972    /// Renders a visual and its children into a new bitmap, on the render
973    /// thread with the next batch, after the composition targets are
974    /// rendered (`CreateCompositionVisualSnapshot`).
975    ///
976    /// The task is faulted with a [`CompositionVisualSnapshotError`] when
977    /// the visual belongs to another compositor or is not attached to a
978    /// composition target (`InvalidOperationException` upstream, which the
979    /// asynchronous method also reports through its task).
980    pub fn create_composition_visual_snapshot(&self, visual: &CompositionVisual, scaling: f64) -> ServerJobTask<Bitmap> {
981        if !Rc::ptr_eq(visual.compositor(), &self.this()) {
982            return ServerJobTask::from_exception(Arc::new(CompositionVisualSnapshotError(
983                "the visual belongs to another compositor",
984            )));
985        }
986        if visual.root().is_none() {
987            return ServerJobTask::from_exception(Arc::new(CompositionVisualSnapshotError(
988                "the visual is not attached to a composition target",
989            )));
990        }
991        // The job names the server visual by id, as the other jobs of an object do: the id
992        // stays bound to the server visual while the visual is alive.
993        let server = visual.server();
994        self.invoke_server_job_async(
995            move |compositor| match compositor.get::<ServerCompositionVisual>(server) {
996                Some(visual) => Ok(Bitmap::from_impl(compositor.create_composition_visual_snapshot(&visual, scaling, true))),
997                None => {
998                    let error: ServerJobError =
999                        Arc::new(CompositionVisualSnapshotError("the server visual no longer exists"));
1000                    Err(error)
1001                }
1002            },
1003            true,
1004        )
1005    }
1006
1007    /// `GetRenderInterfacePublicFeatures`: the public features of the
1008    /// render interface, handed to `f` inside the compositor lock; what `f`
1009    /// takes from them is the outcome.
1010    ///
1011    /// As upstream: the features that are cached are the answer at once; a
1012    /// compositor whose loop does not run in the background asks the server
1013    /// on this thread (which creates the backend context when needed); a
1014    /// compositor confined to its render thread without a cache asks with a
1015    /// job of the render thread (`InvokeServerJobAsync(Server.RT_GetRenderInterfaceFeatures)`)
1016    /// and the outcome comes with the job, so the caller awaits it. `f` then
1017    /// runs in the job, on the render thread.
1018    ///
1019    /// Upstream hands the dictionary itself to the caller, and the caller
1020    /// takes the feature from it; here a feature is an `Rc` of the server
1021    /// side, so `f` takes what it needs inside the lock, where the features
1022    /// are, and only what it returns leaves. Its third argument tells
1023    /// whether the calling thread may create the backend context: `false`
1024    /// only on the thread of a confined compositor, which answers from the
1025    /// cache. Without a backend context a compositor that is not confined
1026    /// and whose render interface is not ready has no features (`None`).
1027    fn get_render_interface_public_features<R: Send + 'static>(
1028        &self,
1029        f: impl FnOnce(&ServerCompositor, &HashMap<std::any::TypeId, Rc<dyn std::any::Any>>, bool) -> R + Send + 'static,
1030    ) -> LocalFuture<Option<R>> {
1031        self.dispatcher.verify_access();
1032        if self.is_confined_to_render_thread() {
1033            // The features are objects of the server side: they are read
1034            // under the lock, and `f` runs there; it is only taken out of
1035            // its cell when there is a cache to give it.
1036            let f = Cell::new(Some(f));
1037            let cached = self.server.with(|server| {
1038                let features = server.at_try_get_cached_render_interface_features()?;
1039                let f = f.take().expect("taken once");
1040                Some(f(server, &features, false))
1041            });
1042            if let Some(result) = cached {
1043                return Box::pin(std::future::ready(Some(result)));
1044            }
1045            let f = f.take().expect("taken once");
1046            let task = self.invoke_server_job_async(
1047                move |server| {
1048                    let features = server.rt_get_render_interface_features();
1049                    Ok(f(server, &features, true))
1050                },
1051                false,
1052            );
1053            return Box::pin(ServerJobResult::new(task));
1054        }
1055        let result = self.server.with(|server| {
1056            if let Some(features) = server.at_try_get_cached_render_interface_features() {
1057                return Some(f(server, &features, true));
1058            }
1059            if !server.render_interface().is_ready() {
1060                return None;
1061            }
1062            let features = server.rt_get_render_interface_features();
1063            Some(f(server, &features, true))
1064        });
1065        Box::pin(std::future::ready(result))
1066    }
1067
1068    /// Attempts to query for a feature from the platform render interface
1069    /// (`TryGetRenderInterfaceFeature`): a future that completes with the
1070    /// feature, or `None` when the render interface has none. Awaited on the
1071    /// thread of the compositor; see
1072    /// [`get_render_interface_public_features`](Self::get_render_interface_public_features)
1073    /// for when it completes.
1074    ///
1075    /// Upstream hands out the feature object itself, which the two threads
1076    /// then share. Here the feature is an `Rc` of the server side, so what
1077    /// is handed out is a [`RenderInterfaceFeature`](super::RenderInterfaceFeature):
1078    /// a handle to the same feature that stays inside the compositor lock
1079    /// and lends the feature there. It keeps the feature alive as the
1080    /// object upstream does.
1081    pub fn try_get_render_interface_feature(
1082        &self,
1083        feature_type: std::any::TypeId,
1084    ) -> LocalFuture<Option<super::RenderInterfaceFeature>> {
1085        // The handle is cloned from the map and bound to the lock inside
1086        // the lock: no count of the server side is touched outside it.
1087        let locked = self.server.clone();
1088        let features = self.get_render_interface_public_features(move |server, features, _| {
1089            let feature = features.get(&feature_type)?;
1090            Some(super::RenderInterfaceFeature::new(super::server::LockBound::new(&locked, server, feature.clone())))
1091        });
1092        Box::pin(async move { features.await.flatten() })
1093    }
1094
1095    /// Attempts to query for the GPU interop feature from the platform
1096    /// render interface (`TryGetCompositionGpuInterop`): a future that
1097    /// completes with the interop with GPU objects created outside of the
1098    /// framework, or `None` when the render interface does not support it.
1099    /// Awaited on the thread of the compositor.
1100    ///
1101    /// Upstream awaits the feature (`TryGetRenderInterfaceFeature`) and
1102    /// constructs the interop where the await continues: on the render
1103    /// thread, when a job of the render thread gave the feature. Here the
1104    /// feature is an object of the server side: the server part of the
1105    /// interop is made from it inside the lock, where the feature is read
1106    /// (in that job, for a confined compositor without a cache), and this
1107    /// thread makes the interop around it.
1108    pub fn try_get_composition_gpu_interop(&self) -> LocalFuture<Option<Rc<dyn super::ICompositionGpuInterop>>> {
1109        let feature_type = std::any::TypeId::of::<dyn crate::platform::IExternalObjectsRenderInterfaceContextFeature>();
1110        let locked = self.server.clone();
1111        let parts = self.get_render_interface_public_features(move |server, features, may_create_context| {
1112            let external_objects = features
1113                .get(&feature_type)?
1114                .downcast_ref::<Rc<dyn crate::platform::IExternalObjectsRenderInterfaceContextFeature>>()?
1115                .clone();
1116            // `None` where this thread may not create the context of the
1117            // render interface and the render thread has none at present.
1118            super::CompositionInterop::server_part(&locked, server, external_objects, may_create_context)
1119        });
1120        let this = self.this();
1121        Box::pin(async move {
1122            let part = parts.await.flatten()?;
1123            let interop: Rc<dyn super::ICompositionGpuInterop> = super::CompositionInterop::from_server_part(&this, part);
1124            Some(interop)
1125        })
1126    }
1127
1128    /// Whether an object is queued for serialization (for unit tests).
1129    pub fn unit_test_is_registered_for_serialization(&self, serializable: &dyn ICompositorSerializable) -> bool {
1130        self.object_serialization_hash_set.borrow().contains(&serializable.serialization_key())
1131    }
1132
1133    /// The compositor registered in the service locator, if any.
1134    pub fn try_get_default_compositor() -> Option<Rc<Compositor>> {
1135        FerroLocator::current().get_service::<Compositor>()
1136    }
1137}
1138
1139impl Drop for Compositor {
1140    fn drop(&mut self) {
1141        match &self.confined_loop_task {
1142            Some(task) => {
1143                // The graph holds what only the render thread may drop (the
1144                // render targets, the graphics context), so its release is
1145                // the last job of that thread: the task stays in the loop
1146                // for one more tick, releases the graph there and then has
1147                // itself removed. Asked for before the lock is entered: no
1148                // frame starts after this thread has been inside.
1149                task.request_release();
1150                // What the graph shares with this thread is dropped here,
1151                // for the same reason the other modes release all of it
1152                // here: the count of the handle of the render interface
1153                // belongs to this thread. (The platform graphics and their
1154                // ready state are shared by their contracts and go with it.)
1155                self.server.try_with(|server| server.render_interface().release_platform_handles());
1156                // A loop that does not tick again keeps the task, and the
1157                // graph with it: whichever thread drops the loop then leaks
1158                // the graph instead of dropping it (`LockedServerCompositor`).
1159                self.render_loop.wakeup();
1160            }
1161            None => {
1162                self.render_loop.remove(&self.loop_task);
1163                // The server compositor is released here, on the thread of
1164                // the compositor: a tick in progress on the render thread
1165                // may still hold the task, and with it the lock object, but
1166                // not the graph.
1167                self.server.release();
1168            }
1169        }
1170        // No frame of this compositor applies a batch from here on. The
1171        // batches that are still in the queue are completed as they are:
1172        // the media context commits no compositor while one of the batches
1173        // it committed is not processed.
1174        self.batches.complete_all();
1175        let key = self.key;
1176        // The registry may already be gone when the thread is exiting.
1177        let _ = COMPOSITORS.try_with(|compositors| {
1178            if let Ok(mut compositors) = compositors.try_borrow_mut() {
1179                compositors.remove(&key);
1180            }
1181        });
1182    }
1183}
1184
1185/// A server object listed for disposal by the next batch: the server
1186/// object is disposed when `dispose` is set, and dropped by the server
1187/// compositor, its id becoming free, when `release` is set.
1188struct PendingDisposal {
1189    id: ServerObjectId,
1190    dispose: bool,
1191    release: bool,
1192}
1193
1194/// The error of a snapshot that cannot be taken (`InvalidOperationException`):
1195/// see [`Compositor::create_composition_visual_snapshot`].
1196#[derive(Clone, Debug)]
1197pub struct CompositionVisualSnapshotError(&'static str);
1198
1199impl std::fmt::Display for CompositionVisualSnapshotError {
1200    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1201        f.write_str(self.0)
1202    }
1203}
1204
1205impl std::error::Error for CompositionVisualSnapshotError {}
1206
1207/// A job waiting for the next batch.
1208enum PendingServerJob {
1209    Job(ServerJob),
1210    ObjectJob(ServerObjectId, ServerObjectJob),
1211}
1212
1213/// The error of a job run on the render thread: it crosses to the thread
1214/// that asked for the job.
1215pub type ServerJobError = Arc<dyn std::error::Error + Send + Sync>;
1216
1217/// The outcome of a job run on the render thread: what
1218/// [`Compositor::invoke_server_job_async`] returns (a task upstream).
1219///
1220/// The handle is shared by its clones; the job completes it once, from the
1221/// render thread. Continuations belong to the thread that created the task
1222/// and run there: right away when the job completes on that thread,
1223/// otherwise from its dispatcher.
1224pub struct ServerJobTask<T> {
1225    shared: Arc<ServerJobTaskShared<T>>,
1226}
1227
1228struct ServerJobTaskShared<T> {
1229    outcome: std::sync::Mutex<ServerJobOutcome<T>>,
1230    continuations: crate::utilities::ThreadBound<RefCell<Vec<Box<dyn FnOnce()>>>>,
1231    thread: std::thread::ThreadId,
1232}
1233
1234enum ServerJobOutcome<T> {
1235    Running,
1236    RanToCompletion(Option<T>),
1237    Faulted(ServerJobError),
1238}
1239
1240impl<T> Clone for ServerJobTask<T> {
1241    fn clone(&self) -> Self {
1242        Self { shared: self.shared.clone() }
1243    }
1244}
1245
1246impl<T: Send + 'static> ServerJobTask<T> {
1247    fn with_outcome(outcome: ServerJobOutcome<T>) -> Self {
1248        Self {
1249            shared: Arc::new(ServerJobTaskShared {
1250                outcome: std::sync::Mutex::new(outcome),
1251                continuations: crate::utilities::ThreadBound::new(RefCell::new(Vec::new())),
1252                thread: std::thread::current().id(),
1253            }),
1254        }
1255    }
1256
1257    pub(crate) fn new() -> Self {
1258        Self::with_outcome(ServerJobOutcome::Running)
1259    }
1260
1261    /// A task that has completed with `result`.
1262    pub fn from_result(result: T) -> Self {
1263        Self::with_outcome(ServerJobOutcome::RanToCompletion(Some(result)))
1264    }
1265
1266    /// A task that has failed with `error`.
1267    pub fn from_exception(error: ServerJobError) -> Self {
1268        Self::with_outcome(ServerJobOutcome::Faulted(error))
1269    }
1270
1271    fn run_continuations(shared: &ServerJobTaskShared<T>) {
1272        let continuations = std::mem::take(&mut *shared.continuations.get().borrow_mut());
1273        for continuation in continuations {
1274            continuation();
1275        }
1276    }
1277
1278    fn complete(&self, outcome: ServerJobOutcome<T>) {
1279        {
1280            let mut current = self.shared.outcome.lock().unwrap();
1281            if !matches!(*current, ServerJobOutcome::Running) {
1282                return;
1283            }
1284            *current = outcome;
1285        }
1286
1287        if self.shared.continuations.is_on_thread() {
1288            Self::run_continuations(&self.shared);
1289        } else if let Some(dispatcher) = Dispatcher::from_thread(self.shared.thread) {
1290            // Completed by the render thread: the continuations run on the
1291            // thread of the task.
1292            let shared = self.shared.clone();
1293            dispatcher.post(move || Self::run_continuations(&shared), DispatcherPriority::SEND);
1294        }
1295    }
1296
1297    pub(crate) fn set_result(&self, result: T) {
1298        self.complete(ServerJobOutcome::RanToCompletion(Some(result)))
1299    }
1300
1301    pub(crate) fn set_exception(&self, error: ServerJobError) {
1302        self.complete(ServerJobOutcome::Faulted(error))
1303    }
1304
1305    /// Whether the job has run.
1306    pub fn is_completed(&self) -> bool {
1307        !matches!(*self.shared.outcome.lock().unwrap(), ServerJobOutcome::Running)
1308    }
1309
1310    /// Whether the job has run without an error.
1311    pub fn is_completed_successfully(&self) -> bool {
1312        matches!(*self.shared.outcome.lock().unwrap(), ServerJobOutcome::RanToCompletion(_))
1313    }
1314
1315    /// Whether the job failed.
1316    pub fn is_faulted(&self) -> bool {
1317        matches!(*self.shared.outcome.lock().unwrap(), ServerJobOutcome::Faulted(_))
1318    }
1319
1320    /// The error of a failed job.
1321    pub fn exception(&self) -> Option<ServerJobError> {
1322        match &*self.shared.outcome.lock().unwrap() {
1323            ServerJobOutcome::Faulted(error) => Some(error.clone()),
1324            _ => None,
1325        }
1326    }
1327
1328    /// Takes the result of a job that ran to completion: `None` while it
1329    /// runs, and once taken; the error of a failed job.
1330    pub fn take_result(&self) -> Option<Result<T, ServerJobError>> {
1331        match &mut *self.shared.outcome.lock().unwrap() {
1332            ServerJobOutcome::Running => None,
1333            ServerJobOutcome::RanToCompletion(result) => result.take().map(Ok),
1334            ServerJobOutcome::Faulted(error) => Some(Err(error.clone())),
1335        }
1336    }
1337
1338    /// Runs `continuation` once the job has run; right away if it has.
1339    /// Called on the thread that created the task.
1340    pub fn on_completed(&self, continuation: impl FnOnce() + 'static) {
1341        {
1342            // The outcome is read under its lock, so a job completing on the
1343            // render thread either sees the continuation or has completed.
1344            let outcome = self.shared.outcome.lock().unwrap();
1345            if matches!(*outcome, ServerJobOutcome::Running) {
1346                self.shared.continuations.get().borrow_mut().push(Box::new(continuation));
1347                return;
1348            }
1349        }
1350        continuation();
1351    }
1352}
1353
1354/// A future of the thread of the compositor: what its asynchronous members
1355/// return (a `ValueTask` upstream), awaited on that thread.
1356pub type LocalFuture<T> = std::pin::Pin<Box<dyn std::future::Future<Output = T>>>;
1357
1358/// `await` of the task of a job of the render thread: the result of the
1359/// job. A job that fails ends the awaiting method with its error, as the
1360/// exception that `await` rethrows upstream.
1361struct ServerJobResult<T> {
1362    task: ServerJobTask<T>,
1363    registered: bool,
1364}
1365
1366impl<T: Send + 'static> ServerJobResult<T> {
1367    fn new(task: ServerJobTask<T>) -> Self {
1368        Self { task, registered: false }
1369    }
1370}
1371
1372impl<T: Send + 'static> std::future::Future for ServerJobResult<T> {
1373    type Output = Option<T>;
1374
1375    fn poll(mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output> {
1376        match self.task.take_result() {
1377            Some(Ok(result)) => return std::task::Poll::Ready(Some(result)),
1378            Some(Err(error)) => panic!("{error}"),
1379            None => {}
1380        }
1381        if !self.registered {
1382            self.registered = true;
1383            let waker = cx.waker().clone();
1384            self.task.on_completed(move || waker.wake());
1385        }
1386        std::task::Poll::Pending
1387    }
1388}