Skip to main content

ferroui_base/rendering/composition/
compositing_renderer.rs

1use super::drawing::RenderDataDrawingContext;
2use super::hit_testing::{GeometryCompositionHitTester, ICompositionHitTester, PointCompositionHitTester};
3use super::server::RenderSurfaces;
4use super::{
5    CompositionDrawListVisual, CompositionTarget, CompositionTransparencyLevel, CompositionVisual, Compositor,
6};
7use crate::media::{DrawingContext, Geometry, GeometryHitTestResult, IntersectionResult, MediaContext};
8use crate::reactive::{Disposable, IDisposable};
9use crate::rendering::{
10    IHitTester, IPresentationSource, IRenderer, RendererDiagnostics, SceneInvalidatedEventArgs,
11};
12use crate::threading::DispatcherPriority;
13use crate::utilities::HandlerList;
14use crate::{Point, Rect, Ref, Size, Visual};
15use std::any::{Any, TypeId};
16use std::cell::{Cell, RefCell};
17use std::collections::{HashMap, HashSet};
18use std::rc::{Rc, Weak};
19use std::sync::atomic::{AtomicU64, Ordering};
20
21static NEXT_RENDERER_KEY: AtomicU64 = AtomicU64::new(1);
22
23crate::ferro_thread_local! {
24    /// The renderers of this thread by key: lets the `Send` continuation of
25    /// a batch find its renderer once it is back on the thread.
26    static RENDERERS: RefCell<HashMap<u64, Weak<CompositingRenderer>>> = RefCell::new(HashMap::new());
27}
28
29/// A set of visuals that keeps the order in which they were added.
30///
31/// A removal drops the key at once and leaves its entry in the list, which is compacted when the
32/// stale entries are half of it or the set is read, so that removing is amortised constant time,
33/// as in the reference's `HashSet<Visual>`, instead of a scan of the list for every visual that is
34/// invalidated while it has no composition visual (DEVIATIONS.md, corrected divergences). The
35/// visuals the set yields are unchanged; a removed visual is released at the next compaction.
36#[derive(Default)]
37struct VisualSet {
38    items: Vec<Ref<Visual>>,
39    keys: HashSet<*const Visual>,
40    /// The entries of `items` whose key was removed.
41    stale: usize,
42}
43
44impl VisualSet {
45    fn add(&mut self, visual: &Visual) {
46        if self.keys.insert(visual as *const Visual) {
47            self.items.push(visual.to_ref());
48        }
49    }
50
51    fn contains(&self, visual: &Visual) -> bool {
52        self.keys.contains(&(visual as *const Visual))
53    }
54
55    fn remove(&mut self, visual: &Visual) {
56        let key = visual as *const Visual;
57        if self.keys.remove(&key) {
58            self.stale += 1;
59            if self.stale * 2 >= self.items.len() {
60                self.compact();
61            }
62        }
63    }
64
65    /// Drops the stale entries (and an entry added again after its removal is kept once, at its
66    /// last place).
67    fn compact(&mut self) {
68        let mut seen = HashSet::with_capacity(self.keys.len());
69        let mut items = std::mem::take(&mut self.items);
70        items.reverse();
71        items.retain(|item| {
72            let key = &**item as *const Visual;
73            self.keys.contains(&key) && seen.insert(key)
74        });
75        items.reverse();
76        self.items = items;
77        self.stale = 0;
78    }
79
80    /// The visuals of the set, in the order in which they were added.
81    fn snapshot(&mut self) -> Vec<Ref<Visual>> {
82        if self.stale > 0 {
83            self.compact();
84        }
85        self.items.clone()
86    }
87
88    fn is_empty(&self) -> bool {
89        self.keys.is_empty()
90    }
91
92    fn clear(&mut self) {
93        self.items.clear();
94        self.keys.clear();
95        self.stale = 0;
96    }
97}
98
99/// A renderer that utilizes a [`Compositor`] to render the visual tree.
100pub struct CompositingRenderer {
101    this: Weak<CompositingRenderer>,
102    key: u64,
103    root: Weak<dyn IPresentationSource>,
104    compositor: Rc<Compositor>,
105    recorder: RefCell<RenderDataDrawingContext>,
106    dirty: RefCell<VisualSet>,
107    recalculate_children: RefCell<VisualSet>,
108    queued_update: Cell<bool>,
109    queued_scene_invalidation: Cell<bool>,
110    updating: Cell<bool>,
111    is_disposed: Cell<bool>,
112    composition_target: Rc<CompositionTarget>,
113    diagnostics: Rc<RendererDiagnostics>,
114    diagnostics_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
115    scene_invalidated: HandlerList<dyn Fn(&SceneInvalidatedEventArgs)>,
116}
117
118impl CompositingRenderer {
119    /// Creates the renderer of the tree hosted by `root`. `surfaces`
120    /// returns the platform surfaces to render to.
121    ///
122    /// The renderer refers to its presentation source weakly: the source
123    /// owns its renderer.
124    pub fn new(
125        root: &Rc<dyn IPresentationSource>,
126        compositor: &Rc<Compositor>,
127        surfaces: RenderSurfaces,
128    ) -> Rc<CompositingRenderer> {
129        Self::with_weak_root(Rc::downgrade(root), compositor, surfaces)
130    }
131
132    /// Like [`new`](Self::new), for a presentation source that is still
133    /// being constructed.
134    pub fn with_weak_root(
135        root: Weak<dyn IPresentationSource>,
136        compositor: &Rc<Compositor>,
137        surfaces: RenderSurfaces,
138    ) -> Rc<CompositingRenderer> {
139        let key = NEXT_RENDERER_KEY.fetch_add(1, Ordering::SeqCst);
140        let renderer = Rc::new_cyclic(|this: &Weak<CompositingRenderer>| CompositingRenderer {
141            this: this.clone(),
142            key,
143            root,
144            compositor: compositor.clone(),
145            recorder: RefCell::new(RenderDataDrawingContext::new(Some(compositor.clone()))),
146            dirty: RefCell::new(VisualSet::default()),
147            recalculate_children: RefCell::new(VisualSet::default()),
148            queued_update: Cell::new(false),
149            queued_scene_invalidation: Cell::new(false),
150            updating: Cell::new(false),
151            is_disposed: Cell::new(false),
152            composition_target: compositor.create_composition_target(surfaces),
153            diagnostics: RendererDiagnostics::new(),
154            diagnostics_subscription: RefCell::new(None),
155            scene_invalidated: HandlerList::new(),
156        });
157        let weak = Rc::downgrade(&renderer);
158        let subscription = renderer.diagnostics.property_changed(move |diagnostics, property_name| {
159            if let Some(renderer) = weak.upgrade() {
160                renderer.on_diagnostics_property_changed(diagnostics, property_name);
161            }
162        });
163        *renderer.diagnostics_subscription.borrow_mut() = Some(subscription);
164        RENDERERS.with(|renderers| {
165            let mut renderers = renderers.borrow_mut();
166            renderers.retain(|_, r| r.strong_count() > 0);
167            renderers.insert(key, Rc::downgrade(&renderer));
168        });
169        renderer
170    }
171
172    /// The composition target the renderer draws the tree to.
173    pub fn composition_target(&self) -> &Rc<CompositionTarget> {
174        &self.composition_target
175    }
176
177    /// The compositor of the renderer.
178    pub fn compositor_handle(&self) -> &Rc<Compositor> {
179        &self.compositor
180    }
181
182    /// Sets the visual whose composition visual is the root of the
183    /// composition target; `None` detaches the tree.
184    pub fn set_root(&self, root: Option<Ref<Visual>>) {
185        let visual = root.and_then(|root| root.composition_visual()).map(|visual| (*visual).clone());
186        self.composition_target.set_root(visual);
187    }
188
189    /// Sets the transparency level the composition target renders with.
190    pub fn set_transparency_level(&self, level: CompositionTransparencyLevel) {
191        self.composition_target.set_transparency_level(level);
192    }
193
194    /// Sets the platform-specific scene info passed to the render target.
195    pub fn set_platform_specific_scene_info(&self, info: Option<std::sync::Arc<dyn Any + Send + Sync>>) {
196        self.composition_target.set_platform_specific_scene_info(info);
197    }
198
199    /// The renderer as the hit tester of its scene.
200    pub fn hit_tester(&self) -> Option<Rc<dyn IHitTester>> {
201        self.this.upgrade().map(|this| this as Rc<dyn IHitTester>)
202    }
203
204    pub fn as_any(&self) -> &dyn Any {
205        self
206    }
207
208    fn on_diagnostics_property_changed(&self, diagnostics: &RendererDiagnostics, property_name: &str) {
209        if property_name == RendererDiagnostics::DEBUG_OVERLAYS_PROPERTY_NAME {
210            self.composition_target.set_debug_overlays(diagnostics.debug_overlays());
211        } else if property_name == RendererDiagnostics::LAST_LAYOUT_PASS_TIMING_PROPERTY_NAME {
212            self.composition_target.set_last_layout_pass_timing(diagnostics.last_layout_pass_timing());
213        }
214    }
215
216    fn queue_update(&self) {
217        if self.queued_update.get() {
218            return;
219        }
220        self.queued_update.set(true);
221        let weak = self.this.clone();
222        self.compositor.request_composition_update(move || {
223            if let Some(renderer) = weak.upgrade() {
224                renderer.update();
225            }
226        });
227    }
228
229    fn composition_filter<'a>(
230        filter: Option<&'a dyn Fn(&Visual) -> bool>,
231    ) -> Option<impl Fn(&Rc<CompositionVisual>) -> bool + 'a> {
232        filter.map(|filter| {
233            move |v: &Rc<CompositionVisual>| match CompositionDrawListVisual::from_visual(v) {
234                Some(dlv) => dlv.visual().is_some_and(|visual| filter(&visual)),
235                None => true,
236            }
237        })
238    }
239
240    fn hit_test_all<H: ICompositionHitTester>(
241        &self,
242        input: &H::Input,
243        root: &Visual,
244        filter: Option<&dyn Fn(&Visual) -> bool>,
245    ) -> Vec<(Ref<Visual>, IntersectionResult)> {
246        let Some(root_visual) = root.composition_visual() else { return Vec::new() };
247        let root_visual: Rc<CompositionVisual> = (*root_visual).clone();
248
249        let f = Self::composition_filter(filter);
250        let f = f.as_ref().map(|f| f as &dyn Fn(&Rc<CompositionVisual>) -> bool);
251
252        let Some(res) = self.composition_target.try_hit_test::<H>(input, Some(&root_visual), f) else {
253            return Vec::new();
254        };
255
256        let mut result = Vec::new();
257        for (intersection, v) in res {
258            if let Some(visual) = CompositionDrawListVisual::from_visual(&v).and_then(|dv| dv.visual()) {
259                if filter.is_none_or(|filter| filter(&visual)) {
260                    result.push((visual, intersection));
261                }
262            }
263        }
264        result
265    }
266
267    fn hit_test_first_core<H: ICompositionHitTester>(
268        &self,
269        input: &H::Input,
270        root: &Visual,
271        filter: Option<&dyn Fn(&Visual) -> bool>,
272    ) -> Option<(Ref<Visual>, IntersectionResult)> {
273        let root_visual = root.composition_visual()?;
274        let root_visual: Rc<CompositionVisual> = (*root_visual).clone();
275
276        let f = Self::composition_filter(filter);
277        let f = f.as_ref().map(|f| f as &dyn Fn(&Rc<CompositionVisual>) -> bool);
278        let result_filter = |v: &Rc<CompositionVisual>| CompositionDrawListVisual::from_visual(v).is_some();
279
280        let (hit, intersection) =
281            self.composition_target.try_hit_test_first::<H>(input, Some(&root_visual), f, Some(&result_filter));
282        let visual = CompositionDrawListVisual::from_visual(&hit?)?.visual()?;
283        Some((visual, intersection))
284    }
285
286    fn update_core(&self) {
287        self.queued_update.set(false);
288        let dirty: Vec<Ref<Visual>> = self.dirty.borrow_mut().snapshot();
289        for visual in &dirty {
290            let Some(comp) = visual.composition_visual() else { continue };
291
292            visual.synchronize_composition_properties();
293
294            let draw_list = {
295                struct ResetRecorder<'a>(&'a RefCell<RenderDataDrawingContext>);
296                impl Drop for ResetRecorder<'_> {
297                    fn drop(&mut self) {
298                        if let Ok(mut recorder) = self.0.try_borrow_mut() {
299                            recorder.reset();
300                        }
301                    }
302                }
303                let _reset = ResetRecorder(&self.recorder);
304                let mut recorder = self.recorder.borrow_mut();
305                {
306                    let mut context = DrawingContext::new(&mut *recorder);
307                    visual.render(&mut context);
308                }
309                recorder.get_render_results()
310            };
311            comp.set_draw_list(draw_list);
312
313            visual.synchronize_composition_child_visuals();
314        }
315        let recalculate: Vec<Ref<Visual>> = self.recalculate_children.borrow_mut().snapshot();
316        for visual in &recalculate {
317            if !self.dirty.borrow().contains(visual) {
318                visual.synchronize_composition_child_visuals();
319            }
320        }
321        self.dirty.borrow_mut().clear();
322        self.recalculate_children.borrow_mut().clear();
323        if let Some(root) = self.root.upgrade() {
324            self.composition_target.set_size(root.client_size());
325            self.composition_target.set_scaling(root.render_scaling());
326        }
327
328        let commit = self.compositor.request_composition_batch_commit_async();
329        if !self.queued_scene_invalidation.get() {
330            self.queued_scene_invalidation.set(true);
331            // Updated hit-test information is available after full render
332            let key = self.key;
333            let dispatcher = self.compositor.dispatcher().clone();
334            commit.rendered().on_completed(move || {
335                dispatcher.post(
336                    move || {
337                        let renderer =
338                            RENDERERS.with(|renderers| renderers.borrow().get(&key).and_then(Weak::upgrade));
339                        if let Some(renderer) = renderer {
340                            renderer.queued_scene_invalidation.set(false);
341                            let size = renderer.root.upgrade().map_or(Size::default(), |root| root.client_size());
342                            renderer.raise_scene_invalidated(&SceneInvalidatedEventArgs::new(Rect::from_size(size)));
343                        }
344                    },
345                    DispatcherPriority::INPUT,
346                );
347            });
348        }
349    }
350
351    fn raise_scene_invalidated(&self, e: &SceneInvalidatedEventArgs) {
352        if self.scene_invalidated.is_empty() {
353            return;
354        }
355        for (_, handler) in self.scene_invalidated.snapshot().iter() {
356            handler(e);
357        }
358    }
359
360    pub fn trigger_scene_invalidated_for_unit_tests(&self, rect: Rect) {
361        self.raise_scene_invalidated(&SceneInvalidatedEventArgs::new(rect));
362    }
363
364    /// Whether the renderer holds `visual` for its next update pass. For
365    /// tests only.
366    pub fn has_pending_update_for_unit_tests(&self, visual: &Visual) -> bool {
367        self.dirty.borrow().contains(visual) || self.recalculate_children.borrow().contains(visual)
368    }
369
370    fn update(&self) {
371        if self.updating.get() {
372            return;
373        }
374        if !self.composition_target.is_enabled() {
375            self.queued_update.set(false);
376            return;
377        }
378
379        struct ResetUpdating<'a>(&'a Cell<bool>);
380        impl Drop for ResetUpdating<'_> {
381            fn drop(&mut self) {
382                self.0.set(false);
383            }
384        }
385        self.updating.set(true);
386        let _reset = ResetUpdating(&self.updating);
387        self.update_core();
388    }
389
390    pub fn is_disposed(&self) -> bool {
391        self.is_disposed.get()
392    }
393}
394
395impl IRenderer for CompositingRenderer {
396    fn diagnostics(&self) -> Rc<RendererDiagnostics> {
397        self.diagnostics.clone()
398    }
399
400    fn scene_invalidated(&self, handler: Rc<dyn Fn(&SceneInvalidatedEventArgs)>) -> Rc<dyn IDisposable> {
401        let token = self.scene_invalidated.add(handler);
402        let weak = self.this.clone();
403        Disposable::create(move || {
404            if let Some(this) = weak.upgrade() {
405                this.scene_invalidated.remove(token);
406            }
407        })
408    }
409
410    fn add_dirty(&self, visual: &Visual) {
411        if self.is_disposed.get() {
412            return;
413        }
414        if self.updating.get() {
415            panic!("Visual was invalidated during the render pass");
416        }
417
418        if visual.composition_visual().is_none() {
419            // Queuing a detached visual would keep it alive for as long as rendering is stopped.
420            self.dirty.borrow_mut().remove(visual);
421            self.recalculate_children.borrow_mut().remove(visual);
422        } else {
423            self.dirty.borrow_mut().add(visual);
424        }
425
426        self.queue_update();
427    }
428
429    fn recalculate_children(&self, visual: &Visual) {
430        if self.is_disposed.get() {
431            return;
432        }
433        if self.updating.get() {
434            panic!("Visual was invalidated during the render pass");
435        }
436        self.recalculate_children.borrow_mut().add(visual);
437        self.queue_update();
438    }
439
440    fn resized(&self, _size: Size) {}
441
442    fn paint(&self, _rect: Rect) {
443        if self.is_disposed.get() {
444            return;
445        }
446
447        self.queue_update();
448        self.composition_target.request_redraw();
449        MediaContext::instance().immediate_render_requested(&self.compositor);
450    }
451
452    fn start(&self) {
453        if self.is_disposed.get() {
454            return;
455        }
456
457        self.composition_target.set_is_enabled(true);
458        if !self.dirty.borrow().is_empty() || !self.recalculate_children.borrow().is_empty() {
459            self.queue_update();
460        }
461    }
462
463    fn stop(&self) {
464        self.composition_target.set_is_enabled(false);
465    }
466
467    fn try_get_render_interface_feature(
468        &self,
469        feature_type: TypeId,
470    ) -> super::LocalFuture<Option<super::RenderInterfaceFeature>> {
471        self.compositor.try_get_render_interface_feature(feature_type)
472    }
473
474    fn compositor(&self) -> Option<Rc<Compositor>> {
475        Some(self.compositor.clone())
476    }
477
478    fn dispose(&self) {
479        if self.is_disposed.replace(true) {
480            return;
481        }
482        self.dirty.borrow_mut().clear();
483        self.recalculate_children.borrow_mut().clear();
484        for (token, _) in self.scene_invalidated.snapshot().iter() {
485            self.scene_invalidated.remove(*token);
486        }
487        self.stop();
488        // The tree is released with the target: the root visual and the
489        // target refer to each other.
490        self.composition_target.set_root(None);
491        self.composition_target.mark_disposed_out_of_band();
492        MediaContext::instance().sync_dispose_composition_target(&self.compositor, self.composition_target.server());
493        let key = self.key;
494        let _ = RENDERERS.try_with(|renderers| {
495            if let Ok(mut renderers) = renderers.try_borrow_mut() {
496                renderers.remove(&key);
497            }
498        });
499    }
500}
501
502impl IHitTester for CompositingRenderer {
503    fn hit_test(&self, p: Point, root: &Visual, filter: Option<&dyn Fn(&Visual) -> bool>) -> Vec<Ref<Visual>> {
504        self.hit_test_all::<PointCompositionHitTester>(&p, root, filter).into_iter().map(|(visual, _)| visual).collect()
505    }
506
507    fn hit_test_geometry(
508        &self,
509        geometry: &Geometry,
510        root: &Visual,
511        filter: Option<&dyn Fn(&Visual) -> bool>,
512    ) -> Vec<GeometryHitTestResult> {
513        self.hit_test_all::<GeometryCompositionHitTester>(&geometry.to_ref(), root, filter)
514            .into_iter()
515            .map(|(visual, intersection)| GeometryHitTestResult::new(visual, intersection))
516            .collect()
517    }
518
519    fn hit_test_first(&self, p: Point, root: &Visual, filter: Option<&dyn Fn(&Visual) -> bool>) -> Option<Ref<Visual>> {
520        self.hit_test_first_core::<PointCompositionHitTester>(&p, root, filter).map(|(visual, _)| visual)
521    }
522
523    fn hit_test_first_geometry(
524        &self,
525        geometry: &Geometry,
526        root: &Visual,
527        filter: Option<&dyn Fn(&Visual) -> bool>,
528    ) -> Option<GeometryHitTestResult> {
529        self.hit_test_first_core::<GeometryCompositionHitTester>(&geometry.to_ref(), root, filter)
530            .map(|(visual, intersection)| GeometryHitTestResult::new(visual, intersection))
531    }
532}