Skip to main content

ferroui_base/rendering/composition/server/dirty_rects/
multi_dirty_rect_tracker.rs

1use super::i_dirty_rect_tracker::{full_union, pixel_rect_from_rect_unscaled, VisualizeRandom};
2use super::multi_dirty_rect_tracker_c_dirty_region::CDirtyRegion2;
3use super::{IDirtyRectCollector, IDirtyRectTracker};
4use crate::media::immutable::ImmutableSolidColorBrush;
5use crate::media::Color;
6use crate::platform::{IDrawingContextImpl, IPlatformRenderInterface, IPlatformRenderInterfaceRegion, LtrbRect};
7use crate::Thickness;
8use std::cell::{Cell, Ref, RefCell};
9use std::rc::Rc;
10
11/// Tracks the dirty area as a bounded set of rectangles, merging the ones
12/// that waste the least area when the set is full.
13pub struct MultiDirtyRectTracker {
14    max_overhead: f64,
15    regions: RefCell<CDirtyRegion2>,
16    clip_region: Rc<dyn IPlatformRenderInterfaceRegion>,
17    inflated_rects: RefCell<Vec<LtrbRect>>,
18    random: VisualizeRandom,
19    combined_rect: Cell<LtrbRect>,
20}
21
22impl MultiDirtyRectTracker {
23    pub fn new(platform_render: &dyn IPlatformRenderInterface, max_dirty_rects: i32, max_overhead: f64) -> Self {
24        Self {
25            max_overhead,
26            regions: RefCell::new(CDirtyRegion2::new(max_dirty_rects)),
27            clip_region: platform_render.create_region(),
28            inflated_rects: RefCell::new(Vec::new()),
29            random: VisualizeRandom::new(),
30            combined_rect: Cell::new(LtrbRect::default()),
31        }
32    }
33
34    /// The dirty rectangles of the last finalized frame, inflated and
35    /// clipped to the frame bounds.
36    pub fn inflated_rects(&self) -> Ref<'_, [LtrbRect]> {
37        Ref::map(self.inflated_rects.borrow(), |rects| rects.as_slice())
38    }
39}
40
41impl IDirtyRectCollector for MultiDirtyRectTracker {
42    fn add_rect(&self, rect: LtrbRect) {
43        self.regions.borrow_mut().add(rect);
44    }
45}
46
47impl IDirtyRectTracker for MultiDirtyRectTracker {
48    fn finalize_frame(&self, bounds: LtrbRect) {
49        let mut inflated_rects = self.inflated_rects.borrow_mut();
50        inflated_rects.clear();
51        self.clip_region.reset();
52
53        let mut regions = self.regions.borrow_mut();
54        let dirty_regions = regions.get_uninflated_dirty_regions();
55
56        let mut combined: Option<LtrbRect> = None;
57        for rect in dirty_regions {
58            let inflated = rect.inflate(Thickness::uniform(1.0)).intersect_or_empty(bounds);
59            inflated_rects.push(inflated);
60            self.clip_region.add_rect(pixel_rect_from_rect_unscaled(inflated));
61            combined = full_union(combined, Some(inflated));
62        }
63
64        self.combined_rect.set(combined.unwrap_or_default());
65    }
66
67    fn begin_draw(&self, ctx: &mut dyn IDrawingContextImpl) {
68        ctx.push_clip_region(&*self.clip_region);
69    }
70
71    fn end_draw(&self, ctx: &mut dyn IDrawingContextImpl) {
72        ctx.pop_clip();
73    }
74
75    fn is_empty(&self) -> bool {
76        self.regions.borrow().is_empty()
77    }
78
79    fn intersects(&self, rect: LtrbRect) -> bool {
80        self.inflated_rects.borrow().iter().any(|r| r.intersects(rect))
81    }
82
83    fn initialize(&self, bounds: LtrbRect) {
84        self.regions.borrow_mut().initialize(bounds, self.max_overhead);
85        self.inflated_rects.borrow_mut().clear();
86        self.clip_region.reset();
87        self.combined_rect.set(LtrbRect::default());
88    }
89
90    fn visualize(&self, context: &mut dyn IDrawingContextImpl) {
91        let brush = ImmutableSolidColorBrush::new(Color::new(
92            150,
93            self.random.next(255) as u8,
94            self.random.next(255) as u8,
95            self.random.next(255) as u8,
96        ));
97        context.draw_region(Some(&brush), None, &*self.clip_region);
98    }
99
100    fn combined_rect(&self) -> LtrbRect {
101        self.combined_rect.get()
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::super::i_dirty_rect_tracker::tests::{MockDrawingContext, MockRenderInterface};
108    use super::*;
109    use crate::platform::LtrbPixelRect;
110
111    const BOUNDS: LtrbRect = LtrbRect::new(0.0, 0.0, 1000.0, 1000.0);
112
113    #[test]
114    fn finalize_inflates_by_one_and_clips_to_bounds() {
115        let platform = MockRenderInterface::default();
116        let tracker = MultiDirtyRectTracker::new(&platform, 4, 0.0);
117        let region = platform.regions.borrow()[0].clone();
118        tracker.initialize(BOUNDS);
119        assert!(tracker.is_empty());
120
121        tracker.add_rect(LtrbRect::new(0.0, 0.0, 10.0, 10.0));
122        tracker.add_rect(LtrbRect::new(500.5, 500.5, 510.2, 520.0));
123        assert!(!tracker.is_empty());
124        assert!(tracker.inflated_rects().is_empty());
125
126        tracker.finalize_frame(BOUNDS);
127        // With no allowed overhead the newest rectangle ends up in the first slot.
128        assert_eq!(
129            vec![LtrbRect::new(499.0, 499.0, 512.0, 521.0), LtrbRect::new(0.0, 0.0, 11.0, 11.0)],
130            tracker.inflated_rects().to_vec()
131        );
132        assert_eq!(
133            vec![LtrbPixelRect::new(499, 499, 512, 521), LtrbPixelRect::new(0, 0, 11, 11)],
134            *region.rects.borrow()
135        );
136        assert_eq!(LtrbRect::new(0.0, 0.0, 512.0, 521.0), tracker.combined_rect());
137
138        assert!(tracker.intersects(LtrbRect::new(10.5, 10.5, 20.0, 20.0)));
139        assert!(!tracker.intersects(LtrbRect::new(11.0, 11.0, 499.0, 499.0)));
140        assert!(tracker.intersects(LtrbRect::new(100.0, 100.0, 499.5, 499.5)));
141    }
142
143    #[test]
144    fn initialize_resets_everything() {
145        let platform = MockRenderInterface::default();
146        let tracker = MultiDirtyRectTracker::new(&platform, 4, 0.0);
147        let region = platform.regions.borrow()[0].clone();
148        tracker.initialize(BOUNDS);
149        tracker.add_rect(LtrbRect::new(5.0, 5.0, 10.0, 10.0));
150        tracker.finalize_frame(BOUNDS);
151        assert_eq!(1, region.rects.borrow().len());
152
153        tracker.initialize(BOUNDS);
154        assert!(tracker.is_empty());
155        assert!(tracker.inflated_rects().is_empty());
156        assert!(region.rects.borrow().is_empty());
157        assert_eq!(LtrbRect::default(), tracker.combined_rect());
158        tracker.finalize_frame(BOUNDS);
159        assert_eq!(LtrbRect::default(), tracker.combined_rect());
160    }
161
162    #[test]
163    fn rects_added_before_initialize_are_dropped() {
164        // The surface bounds are empty until `initialize` is called.
165        let platform = MockRenderInterface::default();
166        let tracker = MultiDirtyRectTracker::new(&platform, 4, 0.0);
167        tracker.add_rect(LtrbRect::new(5.0, 5.0, 10.0, 10.0));
168        assert!(tracker.is_empty());
169    }
170
171    #[test]
172    fn invalid_rect_makes_the_whole_surface_dirty() {
173        let platform = MockRenderInterface::default();
174        let tracker = MultiDirtyRectTracker::new(&platform, 4, 0.0);
175        tracker.initialize(BOUNDS);
176        tracker.add_rect(LtrbRect::new(5.0, 5.0, 10.0, 10.0));
177        tracker.add_rect(LtrbRect::new(f64::NAN, 5.0, 10.0, 10.0));
178        tracker.finalize_frame(BOUNDS);
179        // The surface bounds, inflated and clipped back to the bounds.
180        assert_eq!(vec![BOUNDS], tracker.inflated_rects().to_vec());
181        assert_eq!(BOUNDS, tracker.combined_rect());
182    }
183
184    #[test]
185    fn draw_uses_the_region_clip() {
186        let platform = MockRenderInterface::default();
187        let tracker = MultiDirtyRectTracker::new(&platform, 4, 0.0);
188        tracker.initialize(BOUNDS);
189        tracker.add_rect(LtrbRect::new(5.0, 5.0, 10.0, 10.0));
190        tracker.finalize_frame(BOUNDS);
191        let mut ctx = MockDrawingContext::default();
192        tracker.begin_draw(&mut ctx);
193        tracker.end_draw(&mut ctx);
194        tracker.visualize(&mut ctx);
195        assert_eq!(
196            vec![
197                "push_clip_region [LtrbPixelRect { left: 4, top: 4, right: 11, bottom: 11 }]".to_string(),
198                "pop_clip".to_string(),
199                "region alpha 150 rects 1".to_string()
200            ],
201            ctx.log
202        );
203    }
204}