Skip to main content

i_slint_core/
partial_renderer.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! Module for a renderer proxy that tries to render only the parts of the tree that have changed.
5//!
6//! This is the way the partial renderer work:
7//!
8//! 1. [`PartialRenderer::compute_dirty_regions`] will go over the items and try to compute the region that needs to be repainted.
9//!    If either the bounding box has changed, or the PropertyTracker that tracks the rendering properties is dirty, then the
10//!    region is marked dirty.
11//!    That pass also register dependencies on every geometry, and on the non-dirty property trackers.
12//! 2. The Renderer calls [`PartialRenderer::filter_item`] For most items.
13//!    This assume that the cached geometry was requested in the previous step. So it will not register new dependencies.
14//! 3. Then the renderer calls the rendering function for each item that needs to be rendered.
15//!    This register dependencies only on the rendering tracker.
16//!
17
18use crate::Coord;
19use crate::item_rendering::{
20    ItemRenderer, ItemRendererFeatures, RenderBorderRectangle, RenderImage, RenderRectangle,
21    RenderText,
22};
23use crate::item_tree::{ItemTreeRc, ItemTreeWeak, ItemVisitorResult};
24#[cfg(feature = "path")]
25use crate::items::Path;
26use crate::items::{BoxShadow, Clip, ItemRc, ItemRef, Layer, Opacity, RenderingResult, TextInput};
27use crate::lengths::{
28    ItemTransform, LogicalBorderRadius, LogicalPoint, LogicalPx, LogicalRect, LogicalSize,
29    LogicalVector, ScaleFactor,
30};
31use crate::properties::PropertyTracker;
32use crate::window::WindowAdapter;
33use alloc::boxed::Box;
34use alloc::rc::Rc;
35use core::cell::{Cell, RefCell};
36use core::pin::Pin;
37
38/// This structure must be present in items that are Rendered and contains information.
39/// Used by the backend.
40#[derive(Default, Debug)]
41#[repr(C)]
42pub struct CachedRenderingData {
43    /// Used and modified by the backend, should be initialized to 0 by the user code
44    pub(crate) cache_index: Cell<usize>,
45    /// Used and modified by the backend, should be initialized to 0 by the user code.
46    /// The backend compares this generation against the one of the cache to verify
47    /// the validity of the cache_index field.
48    pub(crate) cache_generation: Cell<usize>,
49}
50
51impl CachedRenderingData {
52    /// This function can be used to remove an entry from the rendering cache for a given item, if it
53    /// exists, i.e. if any data was ever cached. This is typically called by the graphics backend's
54    /// implementation of the release_item_graphics_cache function.
55    fn release(
56        &self,
57        cache: &mut PartialRendererCache,
58    ) -> Option<CachedItemBoundingBoxAndTransform> {
59        if self.cache_generation.get() == cache.generation() {
60            let index = self.cache_index.get();
61            self.cache_generation.set(0);
62            Some(cache.remove(index).data)
63        } else {
64            None
65        }
66    }
67
68    /// Return the value if it is in the cache
69    fn get_entry<'a>(
70        &self,
71        cache: &'a mut PartialRendererCache,
72    ) -> Option<&'a mut PartialRenderingCachedData> {
73        let index = self.cache_index.get();
74        if self.cache_generation.get() == cache.generation() { cache.get_mut(index) } else { None }
75    }
76}
77
78/// After rendering an item, we cache the geometry and the transform it applies to
79/// children.
80///
81/// `sibling_index` (the item's rank among all its z-ordered siblings when it was last
82/// visited; compared in `compute_dirty_regions` against the rank counted over the items
83/// that already had a cache entry, so appearing siblings don't shift it) is a `u16` stored
84/// in each variant, so it fits the enum's padding without growing the cache entry on 32- or
85/// 64-bit. It is excluded from geometry comparisons.
86#[derive(Clone)]
87pub enum CachedItemBoundingBoxAndTransform {
88    /// A regular item with a translation
89    RegularItem {
90        /// The item's bounding rect relative to its parent.
91        bounding_rect: LogicalRect,
92        /// The item's offset relative to its parent.
93        offset: LogicalVector,
94        sibling_index: u16,
95    },
96    /// An item such as Rotate that defines an additional transformation
97    ItemWithTransform {
98        /// The item's bounding rect relative to its parent.
99        bounding_rect: LogicalRect,
100        /// The item's transform to apply to children.
101        transform: Box<ItemTransform>,
102        sibling_index: u16,
103    },
104    /// A clip item.
105    ClipItem {
106        /// The item's geometry relative to its parent.
107        geometry: LogicalRect,
108        sibling_index: u16,
109    },
110}
111
112impl CachedItemBoundingBoxAndTransform {
113    fn bounding_rect(&self) -> &LogicalRect {
114        match self {
115            CachedItemBoundingBoxAndTransform::RegularItem { bounding_rect, .. } => bounding_rect,
116            CachedItemBoundingBoxAndTransform::ItemWithTransform { bounding_rect, .. } => {
117                bounding_rect
118            }
119            CachedItemBoundingBoxAndTransform::ClipItem { geometry, .. } => geometry,
120        }
121    }
122
123    fn transform(&self) -> ItemTransform {
124        match self {
125            CachedItemBoundingBoxAndTransform::RegularItem { offset, .. } => {
126                ItemTransform::translation(offset.x as f32, offset.y as f32)
127            }
128            CachedItemBoundingBoxAndTransform::ItemWithTransform { transform, .. } => **transform,
129            CachedItemBoundingBoxAndTransform::ClipItem { geometry, .. } => {
130                ItemTransform::translation(geometry.origin.x as f32, geometry.origin.y as f32)
131            }
132        }
133    }
134
135    fn sibling_index(&mut self) -> &mut u16 {
136        match self {
137            CachedItemBoundingBoxAndTransform::RegularItem { sibling_index, .. }
138            | CachedItemBoundingBoxAndTransform::ItemWithTransform { sibling_index, .. }
139            | CachedItemBoundingBoxAndTransform::ClipItem { sibling_index, .. } => sibling_index,
140        }
141    }
142
143    /// Compare the geometry (bounding rect, transform, clip), ignoring `sibling_index`.
144    fn same_geometry(&self, other: &Self) -> bool {
145        use CachedItemBoundingBoxAndTransform::*;
146        match (self, other) {
147            (
148                RegularItem { bounding_rect: a, offset: oa, .. },
149                RegularItem { bounding_rect: b, offset: ob, .. },
150            ) => a == b && oa == ob,
151            (
152                ItemWithTransform { bounding_rect: a, transform: ta, .. },
153                ItemWithTransform { bounding_rect: b, transform: tb, .. },
154            ) => a == b && ta == tb,
155            (ClipItem { geometry: a, .. }, ClipItem { geometry: b, .. }) => a == b,
156            _ => false,
157        }
158    }
159
160    fn new<T: ItemRendererFeatures>(
161        item_rc: &ItemRc,
162        window_adapter: &Rc<dyn WindowAdapter>,
163        sibling_index: u16,
164    ) -> Self {
165        let geometry = item_rc.geometry();
166
167        if item_rc.borrow().as_ref().clips_children() {
168            return Self::ClipItem { geometry, sibling_index };
169        }
170
171        // Evaluate the bounding rect untracked, as properties that affect the bounding rect are already tracked
172        // at rendering time.
173        let bounding_rect = crate::properties::evaluate_no_tracking(|| {
174            item_rc.bounding_rect(&geometry, window_adapter)
175        });
176
177        if let Some(complex_child_transform) = (T::SUPPORTS_TRANSFORMATIONS
178            && window_adapter.renderer().supports_transformations())
179        .then(|| item_rc.children_transform())
180        .flatten()
181        {
182            Self::ItemWithTransform {
183                bounding_rect,
184                transform: complex_child_transform
185                    .then_translate(geometry.origin.to_vector().cast())
186                    .into(),
187                sibling_index,
188            }
189        } else {
190            Self::RegularItem { bounding_rect, offset: geometry.origin.to_vector(), sibling_index }
191        }
192    }
193}
194
195struct PartialRenderingCachedData {
196    /// The geometry of the item as it was previously rendered.
197    pub data: CachedItemBoundingBoxAndTransform,
198    /// The property tracker that should be used to evaluate whether the item needs to be re-rendered
199    pub tracker: Option<core::pin::Pin<Box<PropertyTracker>>>,
200}
201impl PartialRenderingCachedData {
202    fn new(data: CachedItemBoundingBoxAndTransform) -> Self {
203        Self { data, tracker: None }
204    }
205}
206
207/// The cache that needs to be held by the Window for the partial rendering
208struct PartialRendererCache {
209    slab: slab::Slab<PartialRenderingCachedData>,
210    generation: usize,
211    /// Per ItemTree (keyed by its instance pointer), the union of the clipped screen-space
212    /// regions of the tree's own items, as of the tree's last visit by
213    /// [`PartialRenderer::compute_dirty_regions`]. Nested trees have their own entry.
214    /// When a tree is destroyed, this is the region that needs to be repainted
215    /// (see [`PartialRenderingState::free_graphics_resources`]).
216    tree_screen_rects: alloc::collections::BTreeMap<usize, LogicalRect>,
217}
218
219impl Default for PartialRendererCache {
220    fn default() -> Self {
221        Self { slab: Default::default(), generation: 1, tree_screen_rects: Default::default() }
222    }
223}
224
225impl PartialRendererCache {
226    /// Returns the generation of the cache. The generation starts at 1 and is increased
227    /// whenever the cache is cleared, for example when the GL context is lost.
228    pub fn generation(&self) -> usize {
229        self.generation
230    }
231
232    /// Retrieves a mutable reference to the cached graphics data at index.
233    pub fn get_mut(&mut self, index: usize) -> Option<&mut PartialRenderingCachedData> {
234        self.slab.get_mut(index)
235    }
236
237    /// Inserts data into the cache and returns the index for retrieval later.
238    pub fn insert(&mut self, data: PartialRenderingCachedData) -> usize {
239        self.slab.insert(data)
240    }
241
242    /// Removes the cached graphics data at the given index.
243    pub fn remove(&mut self, index: usize) -> PartialRenderingCachedData {
244        self.slab.remove(index)
245    }
246
247    /// Removes all entries from the cache and increases the cache's generation count, so
248    /// that stale index access can be avoided.
249    pub fn clear(&mut self) {
250        self.slab.clear();
251        self.generation += 1;
252        self.tree_screen_rects.clear();
253    }
254}
255
256/// A region composed of a few rectangles that need to be redrawn.
257#[derive(Default, Clone)]
258pub struct DirtyRegion {
259    rectangles: [euclid::Box2D<Coord, LogicalPx>; Self::MAX_COUNT],
260    count: usize,
261}
262
263impl core::fmt::Debug for DirtyRegion {
264    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
265        write!(f, "{:?}", &self.rectangles[..self.count])
266    }
267}
268
269impl DirtyRegion {
270    /// The maximum number of rectangles that can be stored in a DirtyRegion
271    pub const MAX_COUNT: usize = 3;
272
273    /// An iterator over the part of the region (they can overlap)
274    pub fn iter(&self) -> impl Iterator<Item = euclid::Box2D<Coord, LogicalPx>> + '_ {
275        (0..self.count).map(|x| self.rectangles[x])
276    }
277
278    /// Add a rectangle to the region.
279    ///
280    /// Note that if the region becomes too complex, it might be simplified by being bigger than the actual union.
281    pub fn add_rect(&mut self, rect: LogicalRect) {
282        self.add_box(rect.to_box2d());
283    }
284
285    /// Add a box to the region
286    ///
287    /// Note that if the region becomes too complex, it might be simplified by being bigger than the actual union.
288    pub fn add_box(&mut self, b: euclid::Box2D<Coord, LogicalPx>) {
289        if b.is_empty() {
290            return;
291        }
292        let mut i = 0;
293        while i < self.count {
294            let r = &self.rectangles[i];
295            if r.contains_box(&b) {
296                // the rectangle is already in the union
297                return;
298            } else if b.contains_box(r) {
299                self.rectangles.swap(i, self.count - 1);
300                self.count -= 1;
301                continue;
302            }
303            i += 1;
304        }
305
306        if self.count < Self::MAX_COUNT {
307            self.rectangles[self.count] = b;
308            self.count += 1;
309        } else {
310            let best_merge = (0..self.count)
311                .map(|i| (i, self.rectangles[i].union(&b).area() - self.rectangles[i].area()))
312                .min_by(|a, b| PartialOrd::partial_cmp(&a.1, &b.1).unwrap())
313                .expect("There should always be rectangles")
314                .0;
315            self.rectangles[best_merge] = self.rectangles[best_merge].union(&b);
316        }
317    }
318
319    /// Make an union of two regions.
320    ///
321    /// Note that if the region becomes too complex, it might be simplified by being bigger than the actual union
322    #[must_use]
323    pub fn union(&self, other: &Self) -> Self {
324        let mut s = self.clone();
325        for o in other.iter() {
326            s.add_box(o)
327        }
328        s
329    }
330
331    /// Bounding rectangle of the region.
332    #[must_use]
333    pub fn bounding_rect(&self) -> LogicalRect {
334        if self.count == 0 {
335            return Default::default();
336        }
337        let mut r = self.rectangles[0];
338        for i in 1..self.count {
339            r = r.union(&self.rectangles[i]);
340        }
341        r.to_rect()
342    }
343
344    /// Intersection of a region and a rectangle.
345    #[must_use]
346    pub fn intersection(&self, other: LogicalRect) -> DirtyRegion {
347        let mut ret = self.clone();
348        let other = other.to_box2d();
349        let mut i = 0;
350        while i < ret.count {
351            if let Some(x) = ret.rectangles[i].intersection(&other) {
352                ret.rectangles[i] = x;
353            } else {
354                ret.count -= 1;
355                ret.rectangles.swap(i, ret.count);
356                continue;
357            }
358            i += 1;
359        }
360        ret
361    }
362
363    fn draw_intersects(&self, clipped_geom: LogicalRect) -> bool {
364        let b = clipped_geom.to_box2d();
365        self.iter().any(|r| r.intersects(&b))
366    }
367}
368
369impl From<LogicalRect> for DirtyRegion {
370    fn from(value: LogicalRect) -> Self {
371        let mut s = Self::default();
372        s.add_rect(value);
373        s
374    }
375}
376
377/// This enum describes which parts of the buffer passed to the `SoftwareRenderer` may be re-used to speed up painting.
378// FIXME: #[non_exhaustive] #3023
379#[derive(PartialEq, Eq, Debug, Clone, Default, Copy)]
380pub enum RepaintBufferType {
381    #[default]
382    /// The full window is always redrawn. No attempt at partial rendering will be made.
383    NewBuffer,
384    /// Only redraw the parts that have changed since the previous call to render().
385    ///
386    /// This variant assumes that the same buffer is passed on every call to render() and
387    /// that it still contains the previously rendered frame.
388    ReusedBuffer,
389
390    /// Redraw the part that have changed since the last two frames were drawn.
391    ///
392    /// This is used when using double buffering and swapping of the buffers.
393    SwappedBuffers,
394}
395
396/// Map `rect` (relative to its parent) to screen space through `transform` and clip it
397/// to `clip_rect`. Returns `None` when nothing of the rectangle is visible.
398fn clipped_screen_rect(
399    rect: &LogicalRect,
400    transform: &ItemTransform,
401    clip_rect: &LogicalRect,
402) -> Option<LogicalRect> {
403    #[cfg(not(slint_int_coord))]
404    if !rect.origin.is_finite() {
405        // Account for NaN
406        return None;
407    }
408
409    if rect.is_empty() {
410        return None;
411    }
412    let rect = rect.cast();
413    // Fast path for the common case of a pure translation, so that the per-item,
414    // per-frame calls of `compute_dirty_regions` skip the four-corner transform.
415    let transformed =
416        if (transform.m11, transform.m12, transform.m21, transform.m22) == (1., 0., 0., 1.) {
417            rect.translate(euclid::vec2(transform.m31, transform.m32))
418        } else {
419            transform.outer_transformed_rect(&rect)
420        };
421    transformed.cast().intersection(clip_rect)
422}
423
424/// Put this structure in the renderer to help with partial rendering
425///
426/// This is constructed from a [`PartialRenderingState`]
427pub struct PartialRenderer<'a, T> {
428    cache: &'a RefCell<PartialRendererCache>,
429    /// The region of the screen which is considered dirty and that should be repainted
430    pub dirty_region: DirtyRegion,
431    /// The actual renderer which the drawing call will be forwarded to
432    pub actual_renderer: T,
433    /// The window adapter the renderer is rendering into.
434    pub window_adapter: Rc<dyn WindowAdapter>,
435}
436
437impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> {
438    /// Create a new PartialRenderer
439    fn new(
440        cache: &'a RefCell<PartialRendererCache>,
441        initial_dirty_region: DirtyRegion,
442        actual_renderer: T,
443    ) -> Self {
444        let window_adapter = actual_renderer.window().window_adapter();
445        Self { cache, dirty_region: initial_dirty_region, actual_renderer, window_adapter }
446    }
447
448    /// Visit the tree of item and compute what are the dirty regions
449    pub fn compute_dirty_regions(
450        &mut self,
451        component: &ItemTreeRc,
452        origin: LogicalPoint,
453        size: LogicalSize,
454    ) {
455        #[derive(Clone, Copy)]
456        struct ComputeDirtyRegionState {
457            transform_to_screen: ItemTransform,
458            old_transform_to_screen: ItemTransform,
459            clipped: LogicalRect,
460            must_refresh_children: bool,
461            /// Depth of the item in the tree, used to index `sibling_counters`.
462            depth: usize,
463        }
464
465        // Two counters per tree depth to give each item its rank among its z-ordered siblings.
466        // `.0` counts every visited item and is what gets stored in the cache entry; `.1`
467        // counts only the items that already have a cache entry and is what the stored rank
468        // is compared against. New items are skipped in the comparison rank so that an
469        // appearing sibling does not shift the ranks of the existing items (their overlap
470        // with the new sibling is covered by the new item's own dirty rect), while two
471        // existing items can never trade places without at least one comparison rank
472        // changing.
473        let sibling_counters = RefCell::new(alloc::vec::Vec::<(u16, u16)>::new());
474
475        impl ComputeDirtyRegionState {
476            /// Adjust transform_to_screen and old_transform_to_screen to map from item coordinates
477            /// to the screen when using it on a child, specified by its children transform.
478            fn adjust_transforms_for_child(
479                &mut self,
480                children_transform: &ItemTransform,
481                old_children_transform: &ItemTransform,
482            ) {
483                self.transform_to_screen = children_transform.then(&self.transform_to_screen);
484                self.old_transform_to_screen =
485                    old_children_transform.then(&self.old_transform_to_screen);
486            }
487        }
488
489        crate::item_tree::visit_items(
490            component,
491            crate::item_tree::TraversalOrder::BackToFront,
492            |component, item, index, state| {
493                let mut new_state = *state;
494                let item_rc = ItemRc::new(component.clone(), index);
495
496                let my_sibling_index = {
497                    let depth = state.depth;
498                    let mut counters = sibling_counters.borrow_mut();
499                    if counters.len() <= depth + 1 {
500                        counters.resize(depth + 2, (0, 0));
501                    }
502                    counters[depth + 1] = (0, 0); // this item's children restart at zero
503                    let idx = counters[depth].0;
504                    counters[depth].0 = idx.saturating_add(1);
505                    idx
506                };
507                new_state.depth = state.depth + 1;
508
509                let new_geom = CachedItemBoundingBoxAndTransform::new::<T>(
510                    &item_rc,
511                    &self.window_adapter,
512                    my_sibling_index,
513                );
514
515                // The region the item covers on screen. It is merged into the owning
516                // tree's entry in `tree_screen_rects` so that destroying the tree can
517                // repaint that region (see `PartialRenderingState::free_graphics_resources`),
518                // and it doubles as the item's current-position dirty rect in the branches
519                // below.
520                let new_screen_rect = clipped_screen_rect(
521                    new_geom.bounding_rect(),
522                    &state.transform_to_screen,
523                    &state.clipped,
524                )
525                .unwrap_or_default();
526
527                let rendering_data = item.cached_rendering_data_offset();
528                let mut cache = self.cache.borrow_mut();
529
530                let tree_key = vtable::VRef::as_ptr(crate::item_tree::ItemTreeRc::borrow(component))
531                    .as_ptr() as usize;
532                if index == 0
533                    && let Some(acc) = cache.tree_screen_rects.get_mut(&tree_key)
534                {
535                    // Entering the tree: rebuild its screen region from this pass's visits.
536                    *acc = LogicalRect::default();
537                }
538                if !new_screen_rect.is_empty() {
539                    let acc = cache.tree_screen_rects.entry(tree_key).or_default();
540                    *acc = acc.union(&new_screen_rect);
541                }
542
543                match rendering_data.get_entry(&mut cache) {
544                    Some(PartialRenderingCachedData { data: cached_geom, tracker }) => {
545                        let rendering_dirty = tracker.as_ref().is_some_and(|tr| tr.is_dirty());
546
547                        // Repaint when the rank among the previously known siblings changed,
548                        // in either direction: two items cannot trade places in the stacking
549                        // order with both comparison ranks unchanged, and since an overlap is
550                        // within both items' rects, repainting the changed one(s) covers it.
551                        // Only a decrease is not enough: in a permutation of three or more
552                        // items a pair can flip while one member keeps its rank and the other
553                        // only rises. A saturated rank (>65535 siblings) always repaints.
554                        let comparison_sibling_index = {
555                            let mut counters = sibling_counters.borrow_mut();
556                            let idx = counters[state.depth].1;
557                            counters[state.depth].1 = idx.saturating_add(1);
558                            idx
559                        };
560                        let old_sibling_index =
561                            core::mem::replace(cached_geom.sibling_index(), my_sibling_index);
562                        let sibling_index_changed = my_sibling_index == u16::MAX
563                            || comparison_sibling_index != old_sibling_index;
564                        new_state.must_refresh_children |= sibling_index_changed;
565
566                        let geometry_changed = !cached_geom.same_geometry(&new_geom);
567                        if ItemRef::downcast_pin::<Clip>(item).is_some()
568                            || ItemRef::downcast_pin::<Opacity>(item).is_some()
569                        {
570                            // When the opacity or the clip change, this will impact all the children, including
571                            // the ones outside the element, regardless if they are themselves dirty or not.
572                            new_state.must_refresh_children |= rendering_dirty || geometry_changed;
573
574                            if rendering_dirty {
575                                // Destroy the tracker as we we might not re-render this clipped item but it would stay dirty
576                                *tracker = None;
577                            }
578                        }
579
580                        if geometry_changed {
581                            let old_transform = cached_geom.transform();
582                            self.mark_dirty_rect(
583                                cached_geom.bounding_rect(),
584                                state.old_transform_to_screen,
585                                &state.clipped,
586                            );
587                            self.dirty_region.add_rect(new_screen_rect);
588
589                            new_state
590                                .adjust_transforms_for_child(&new_geom.transform(), &old_transform);
591
592                            *cached_geom = new_geom;
593
594                            return ItemVisitorResult::Continue(new_state);
595                        }
596
597                        new_state.adjust_transforms_for_child(
598                            &cached_geom.transform(),
599                            &cached_geom.transform(),
600                        );
601
602                        let moved = state.must_refresh_children
603                            || sibling_index_changed
604                            || new_state.transform_to_screen != new_state.old_transform_to_screen;
605
606                        if rendering_dirty {
607                            self.dirty_region.add_rect(new_screen_rect);
608                            if moved {
609                                self.mark_dirty_rect(
610                                    cached_geom.bounding_rect(),
611                                    state.old_transform_to_screen,
612                                    &state.clipped,
613                                );
614                            }
615
616                            ItemVisitorResult::Continue(new_state)
617                        } else {
618                            if moved {
619                                self.mark_dirty_rect(
620                                    cached_geom.bounding_rect(),
621                                    state.old_transform_to_screen,
622                                    &state.clipped,
623                                );
624                                self.dirty_region.add_rect(new_screen_rect);
625                            } else if let Some(tr) = &tracker {
626                                tr.as_ref().register_as_dependency_to_current_binding();
627                            }
628
629                            if let CachedItemBoundingBoxAndTransform::ClipItem {
630                                geometry, ..
631                            } = &cached_geom
632                            {
633                                new_state.clipped = new_state
634                                    .clipped
635                                    .intersection(
636                                        &state
637                                            .transform_to_screen
638                                            .outer_transformed_rect(&geometry.cast())
639                                            .cast()
640                                            .union(
641                                                &state
642                                                    .old_transform_to_screen
643                                                    .outer_transformed_rect(&geometry.cast())
644                                                    .cast(),
645                                            ),
646                                    )
647                                    .unwrap_or_default();
648                                if new_state.clipped.is_empty() {
649                                    return ItemVisitorResult::SkipChildren;
650                                }
651                            }
652                            ItemVisitorResult::Continue(new_state)
653                        }
654                    }
655                    None => {
656                        let cache_entry = PartialRenderingCachedData::new(new_geom.clone());
657                        rendering_data.cache_index.set(cache.insert(cache_entry));
658                        rendering_data.cache_generation.set(cache.generation());
659
660                        new_state.adjust_transforms_for_child(
661                            &new_geom.transform(),
662                            &new_geom.transform(),
663                        );
664
665                        if let CachedItemBoundingBoxAndTransform::ClipItem { geometry, .. } =
666                            new_geom
667                        {
668                            new_state.clipped = new_state
669                                .clipped
670                                .intersection(
671                                    &state
672                                        .transform_to_screen
673                                        .outer_transformed_rect(&geometry.cast())
674                                        .cast(),
675                                )
676                                .unwrap_or_default();
677                        }
678
679                        self.dirty_region.add_rect(new_screen_rect);
680                        if new_state.clipped.is_empty() {
681                            ItemVisitorResult::SkipChildren
682                        } else {
683                            ItemVisitorResult::Continue(new_state)
684                        }
685                    }
686                }
687            },
688            {
689                let initial_transform =
690                    euclid::Transform2D::translation(origin.x as f32, origin.y as f32);
691                ComputeDirtyRegionState {
692                    transform_to_screen: initial_transform,
693                    old_transform_to_screen: initial_transform,
694                    clipped: LogicalRect::from_size(size),
695                    must_refresh_children: false,
696                    depth: 0,
697                }
698            },
699        );
700    }
701
702    fn mark_dirty_rect(
703        &mut self,
704        rect: &LogicalRect,
705        transform: ItemTransform,
706        clip_rect: &LogicalRect,
707    ) {
708        if let Some(rect) = clipped_screen_rect(rect, &transform, clip_rect) {
709            self.dirty_region.add_rect(rect);
710        }
711    }
712
713    fn do_rendering(
714        cache: &RefCell<PartialRendererCache>,
715        rendering_data: &CachedRenderingData,
716        item_rc: &ItemRc,
717        render_fn: impl FnOnce(),
718    ) {
719        let mut cache = cache.borrow_mut();
720        if let Some(entry) = rendering_data.get_entry(&mut cache) {
721            entry
722                .tracker
723                .get_or_insert_with(|| Box::pin(PropertyTracker::default()))
724                .as_ref()
725                .evaluate(render_fn);
726        } else {
727            // This item was created between the computation of the dirty region and the actual rendering.
728            // Register a dependency to the geometry since this wasn't done before
729            item_rc.geometry();
730            render_fn();
731        }
732    }
733
734    /// Move the actual renderer
735    pub fn into_inner(self) -> T {
736        self.actual_renderer
737    }
738
739    /// Whether an item with this bounding rect is visible in the clip and dirty region.
740    fn item_is_drawn(&self, item_bounding_rect: &LogicalRect) -> bool {
741        self.get_current_clip().intersection(item_bounding_rect).is_some_and(|clipped_geom| {
742            let screen_geom =
743                self.current_transform().outer_transformed_rect(&clipped_geom.cast()).cast();
744            self.dirty_region.draw_intersects(screen_geom)
745        })
746    }
747}
748
749macro_rules! forward_rendering_call {
750    (fn $fn:ident($Ty:ty) $(-> $Ret:ty)?) => {
751        fn $fn(&mut self, obj: Pin<&$Ty>, item_rc: &ItemRc, size: LogicalSize) $(-> $Ret)? {
752            let mut ret = None;
753            Self::do_rendering(&self.cache, &obj.cached_rendering_data, item_rc, || {
754                ret = Some(self.actual_renderer.$fn(obj, item_rc, size));
755            });
756            ret.unwrap_or_default()
757        }
758    };
759}
760
761macro_rules! forward_rendering_call2 {
762    (fn $fn:ident($Ty:ty) $(-> $Ret:ty)?) => {
763        fn $fn(&mut self, obj: Pin<&$Ty>, item_rc: &ItemRc, size: LogicalSize, cache: &CachedRenderingData) $(-> $Ret)? {
764            let mut ret = None;
765            Self::do_rendering(&self.cache, &cache, item_rc, || {
766                ret = Some(self.actual_renderer.$fn(obj, item_rc, size, &cache));
767            });
768            ret.unwrap_or_default()
769        }
770    };
771}
772
773impl<T: ItemRenderer + ItemRendererFeatures> ItemRenderer for PartialRenderer<'_, T> {
774    fn filter_item(
775        &mut self,
776        item_rc: &ItemRc,
777        window_adapter: &Rc<dyn WindowAdapter>,
778    ) -> (bool, LogicalPoint, Option<LogicalSize>) {
779        let item = item_rc.borrow();
780        let rendering_data = item.cached_rendering_data_offset();
781
782        // The entry is fresh: compute_dirty_regions() refreshes it every frame.
783        let cached = {
784            let mut cache = self.cache.borrow_mut();
785            rendering_data.get_entry(&mut cache).map(|e| {
786                let draw = self.item_is_drawn(e.data.bounding_rect());
787                let offset = match &e.data {
788                    CachedItemBoundingBoxAndTransform::RegularItem { offset, .. } => Some(*offset),
789                    _ => None,
790                };
791                (draw, offset)
792            })
793        };
794
795        // Items that are not drawn only need their origin; this skips e.g. shaping off-screen text.
796        if let Some((false, Some(offset))) = cached {
797            return (false, offset.to_point(), None);
798        }
799
800        // Query untracked, as the bounding rect calculation already registers a dependency on the geometry.
801        let item_geometry = crate::properties::evaluate_no_tracking(|| item_rc.geometry());
802        let draw = cached.map(|(draw, _)| draw).unwrap_or_else(|| {
803            // The item was created between the computation of the dirty region and the
804            // actual rendering.
805            self.item_is_drawn(&item_rc.bounding_rect(&item_geometry, window_adapter))
806        });
807
808        (draw, item_geometry.origin, Some(item_geometry.size))
809    }
810
811    forward_rendering_call2!(fn draw_rectangle(dyn RenderRectangle));
812    forward_rendering_call2!(fn draw_border_rectangle(dyn RenderBorderRectangle));
813    forward_rendering_call2!(fn draw_window_background(dyn RenderRectangle));
814    forward_rendering_call2!(fn draw_image(dyn RenderImage));
815    forward_rendering_call2!(fn draw_text(dyn RenderText));
816    forward_rendering_call!(fn draw_text_input(TextInput));
817    #[cfg(feature = "path")]
818    forward_rendering_call!(fn draw_path(Path));
819    forward_rendering_call!(fn draw_box_shadow(BoxShadow));
820
821    forward_rendering_call!(fn visit_clip(Clip) -> RenderingResult);
822    forward_rendering_call!(fn visit_opacity(Opacity) -> RenderingResult);
823    forward_rendering_call!(fn visit_layer(Layer) -> RenderingResult);
824
825    fn combine_clip(&mut self, rect: LogicalRect, radius: LogicalBorderRadius) -> bool {
826        self.actual_renderer.combine_clip(rect, radius)
827    }
828
829    fn get_current_clip(&self) -> LogicalRect {
830        self.actual_renderer.get_current_clip()
831    }
832
833    fn translate(&mut self, distance: LogicalVector) {
834        self.actual_renderer.translate(distance)
835    }
836    fn current_transform(&self) -> ItemTransform {
837        self.actual_renderer.current_transform()
838    }
839
840    fn rotate(&mut self, angle_in_degrees: f32) {
841        self.actual_renderer.rotate(angle_in_degrees)
842    }
843
844    fn scale(&mut self, x_factor: f32, y_factor: f32) {
845        self.actual_renderer.scale(x_factor, y_factor)
846    }
847
848    fn apply_opacity(&mut self, opacity: f32) {
849        self.actual_renderer.apply_opacity(opacity)
850    }
851
852    fn global_alpha_transparent(&self) -> bool {
853        self.actual_renderer.global_alpha_transparent()
854    }
855
856    fn save_state(&mut self) {
857        self.actual_renderer.save_state()
858    }
859
860    fn restore_state(&mut self) {
861        self.actual_renderer.restore_state()
862    }
863
864    fn scale_factor(&self) -> ScaleFactor {
865        self.actual_renderer.scale_factor()
866    }
867
868    fn draw_cached_pixmap(
869        &mut self,
870        item_rc: &ItemRc,
871        update_fn: &dyn Fn(&mut dyn FnMut(u32, u32, &[u8])),
872    ) {
873        self.actual_renderer.draw_cached_pixmap(item_rc, update_fn)
874    }
875
876    fn draw_string(&mut self, string: &str, color: crate::Color) {
877        self.actual_renderer.draw_string(string, color)
878    }
879
880    fn draw_image_direct(&mut self, image: crate::graphics::image::Image) {
881        self.actual_renderer.draw_image_direct(image)
882    }
883
884    fn window(&self) -> &crate::window::WindowInner {
885        self.actual_renderer.window()
886    }
887
888    fn as_any(&mut self) -> Option<&mut dyn core::any::Any> {
889        self.actual_renderer.as_any()
890    }
891}
892
893/// This struct holds the state of the partial renderer between different frames, in particular the cache of the bounding rect
894/// of each item. This permits a more fine-grained computation of the region that needs to be repainted.
895#[derive(Default)]
896pub struct PartialRenderingState {
897    partial_cache: RefCell<PartialRendererCache>,
898    /// This is the area which we are going to redraw in the next frame, no matter if the items are dirty or not
899    force_dirty: RefCell<DirtyRegion>,
900    /// Force a redraw in the next frame, no matter what's dirty. Use only as a last resort.
901    force_screen_refresh: Cell<bool>,
902}
903
904impl PartialRenderingState {
905    /// Creates a partial renderer that's initialized with the partial rendering caches maintained in this state structure.
906    /// Call [`Self::apply_dirty_region`] after this function to compute the correct partial rendering region.
907    pub fn create_partial_renderer<T: ItemRenderer + ItemRendererFeatures>(
908        &self,
909        renderer: T,
910    ) -> PartialRenderer<'_, T> {
911        PartialRenderer::new(&self.partial_cache, DirtyRegion::default(), renderer)
912    }
913
914    /// Compute the correct partial rendering region based on the components to be drawn, the bounding rectangles of
915    /// changes items within, and the current repaint buffer type. Returns the computed dirty region just for this frame.
916    /// The provided buffer_dirty_region specifies which area of the buffer is known to *additionally* require repainting,
917    /// where `None` means that buffer is not known to be dirty beyond what applies to this frame (reused buffer).
918    pub fn apply_dirty_region<T: ItemRenderer + ItemRendererFeatures>(
919        &self,
920        partial_renderer: &mut PartialRenderer<'_, T>,
921        components: &[(ItemTreeWeak, LogicalPoint)],
922        logical_window_size: LogicalSize,
923        dirty_region_of_existing_buffer: Option<DirtyRegion>,
924    ) -> DirtyRegion {
925        for (component, origin) in components {
926            if let Some(component) = crate::item_tree::ItemTreeWeak::upgrade(component) {
927                partial_renderer.compute_dirty_regions(&component, *origin, logical_window_size);
928            }
929        }
930
931        let screen_region = LogicalRect::from_size(logical_window_size);
932
933        // Collect the regions accumulated in `force_dirty` (destroyed item trees,
934        // `mark_dirty_region` calls) only now: repeater instances are dropped by
935        // `ensure_tree_instantiated` inside `draw_contents`, after the partial renderer
936        // for the frame was already created.
937        partial_renderer.dirty_region =
938            partial_renderer.dirty_region.union(&self.force_dirty.take());
939
940        if self.force_screen_refresh.take() {
941            partial_renderer.dirty_region = screen_region.into();
942        }
943
944        let region_to_repaint = partial_renderer.dirty_region.clone();
945
946        partial_renderer.dirty_region = match dirty_region_of_existing_buffer {
947            Some(dirty_region) => partial_renderer.dirty_region.union(&dirty_region),
948            None => partial_renderer.dirty_region.clone(),
949        }
950        .intersection(screen_region);
951
952        region_to_repaint
953    }
954
955    /// Add the specified region to the list of regions to include in the next rendering.
956    pub fn mark_dirty_region(&self, region: DirtyRegion) {
957        self.force_dirty.replace_with(|r| r.union(&region));
958    }
959
960    /// Call this from your renderer's `free_graphics_resources` function to ensure that the cached item geometries
961    /// are cleared for the destroyed items in the item tree, and that the screen region the tree
962    /// covered is repainted in the next frame.
963    pub fn free_graphics_resources(
964        &self,
965        component: crate::item_tree::ItemTreeRef,
966        items: &mut dyn Iterator<Item = Pin<ItemRef<'_>>>,
967    ) {
968        let mut cache = self.partial_cache.borrow_mut();
969
970        let tree_key = vtable::VRef::as_ptr(component).as_ptr() as usize;
971        if let Some(rect) = cache.tree_screen_rects.remove(&tree_key) {
972            self.force_dirty.borrow_mut().add_rect(rect);
973        }
974
975        for item in items {
976            item.cached_rendering_data_offset().release(&mut cache);
977        }
978    }
979
980    /// Clears the partial rendering cache. Use this for example when the entire underlying window surface changes.
981    pub fn clear_cache(&self) {
982        self.partial_cache.borrow_mut().clear();
983    }
984
985    /// Force re-rendering of the entire window region the next time a partial renderer is created.
986    pub fn force_screen_refresh(&self) {
987        self.force_screen_refresh.set(true);
988    }
989}
990
991#[test]
992fn dirty_region_ignores_empty_rects() {
993    // `compute_dirty_regions` feeds the empty rect of an invisible item into
994    // `add_rect`; it must not drag the region towards the empty rect's origin.
995    let mut region = DirtyRegion::default();
996    region.add_rect(LogicalRect::default());
997    assert_eq!(region.iter().count(), 0);
998    let real = LogicalRect::new(LogicalPoint::new(10., 10.), LogicalSize::new(16., 16.));
999    region.add_rect(real);
1000    region.add_rect(LogicalRect::default());
1001    assert_eq!(region.bounding_rect(), real);
1002}
1003
1004#[test]
1005fn dirty_region_no_intersection() {
1006    let mut region = DirtyRegion::default();
1007    region.add_rect(LogicalRect::new(LogicalPoint::new(10., 10.), LogicalSize::new(16., 16.)));
1008    region.add_rect(LogicalRect::new(LogicalPoint::new(100., 100.), LogicalSize::new(16., 16.)));
1009    region.add_rect(LogicalRect::new(LogicalPoint::new(200., 100.), LogicalSize::new(16., 16.)));
1010    let i = region
1011        .intersection(LogicalRect::new(LogicalPoint::new(50., 50.), LogicalSize::new(10., 10.)));
1012    assert_eq!(i.iter().count(), 0);
1013}