lgui_core/runtime/frame/
dirty.rs1use crate::core::UiRect;
2
3#[derive(Clone, Copy, Debug)]
4pub struct DirtyStrategy {
5 pub max_rects: usize,
6 pub full_area_ratio: f32,
7 pub dirty_outset: f32,
8}
9
10impl Default for DirtyStrategy {
11 fn default() -> Self {
12 Self {
13 max_rects: 12,
14 full_area_ratio: 0.90,
15 dirty_outset: 2.0,
16 }
17 }
18}
19
20#[derive(Clone, Debug)]
21pub struct DirtyRegionSet {
22 viewport: UiRect,
23 rects: Vec<UiRect>,
24 full: bool,
25 fallback_reason: Option<&'static str>,
26 strategy: DirtyStrategy,
27}
28
29impl DirtyRegionSet {
30 pub fn new(viewport: UiRect) -> Self {
31 Self::with_strategy(viewport, DirtyStrategy::default())
32 }
33
34 pub fn with_strategy(viewport: UiRect, strategy: DirtyStrategy) -> Self {
35 Self {
36 viewport,
37 rects: Vec::new(),
38 full: false,
39 fallback_reason: None,
40 strategy,
41 }
42 }
43
44 pub fn full(viewport: UiRect) -> Self {
45 let mut set = Self::new(viewport);
46 set.mark_full();
47 set
48 }
49
50 pub fn mark_full(&mut self) {
51 self.mark_full_with_reason("explicit");
52 }
53
54 pub fn mark_full_with_reason(&mut self, reason: &'static str) {
55 self.full = true;
56 self.fallback_reason = Some(reason);
57 self.rects.clear();
58 }
59
60 pub fn add(&mut self, rect: UiRect) {
61 if self.full {
62 return;
63 }
64 let Some(rect) = rect
65 .inflate(self.strategy.dirty_outset, self.strategy.dirty_outset)
66 .intersect(self.viewport)
67 else {
68 return;
69 };
70 if rect.width() <= 0.0 || rect.height() <= 0.0 {
71 return;
72 }
73
74 let mut merged = rect;
75 let mut index = 0;
76 while index < self.rects.len() {
77 if should_merge(self.rects[index], merged) {
78 merged = self.rects.remove(index).union(merged);
79 } else {
80 index += 1;
81 }
82 }
83 self.rects.push(merged);
84 self.normalize();
85 }
86
87 pub fn add_all(&mut self, rects: impl IntoIterator<Item = UiRect>) {
88 for rect in rects {
89 self.add(rect);
90 }
91 }
92
93 pub fn is_full(&self) -> bool {
94 self.full
95 }
96
97 pub fn is_empty(&self) -> bool {
98 !self.full && self.rects.is_empty()
99 }
100
101 pub fn rects(&self) -> &[UiRect] {
102 &self.rects
103 }
104
105 pub fn rect_count(&self) -> usize {
106 if self.full {
107 1
108 } else {
109 self.rects.len()
110 }
111 }
112
113 pub fn dirty_area(&self) -> f64 {
114 if self.full {
115 area(self.viewport)
116 } else {
117 self.rects.iter().map(|rect| area(*rect)).sum()
118 }
119 }
120
121 pub fn viewport_area(&self) -> f64 {
122 area(self.viewport)
123 }
124
125 pub fn area_ratio(&self) -> f32 {
126 let viewport_area = self.viewport_area();
127 if viewport_area <= 0.0 {
128 return 1.0;
129 }
130 (self.dirty_area() / viewport_area) as f32
131 }
132
133 pub fn viewport(&self) -> UiRect {
134 self.viewport
135 }
136
137 pub fn effective_rects(&self) -> Vec<UiRect> {
138 if self.full {
139 vec![self.viewport]
140 } else {
141 self.rects.clone()
142 }
143 }
144
145 pub fn fallback_reason(&self) -> Option<&'static str> {
146 self.fallback_reason
147 }
148
149 fn normalize(&mut self) {
150 if self.rects.len() > self.strategy.max_rects {
151 self.mark_full_with_reason("too-many-rects");
152 return;
153 }
154 let viewport_area = area(self.viewport);
155 if viewport_area <= 0.0 {
156 self.mark_full_with_reason("invalid-viewport");
157 return;
158 }
159 let dirty_area: f64 = self.rects.iter().map(|rect| area(*rect)).sum();
160 if (dirty_area / viewport_area) as f32 >= self.strategy.full_area_ratio {
161 self.mark_full_with_reason("area-threshold");
162 }
163 }
164}
165
166fn should_merge(a: UiRect, b: UiRect) -> bool {
167 a.intersect(b).is_some() || expanded(a, 2.0).intersect(b).is_some()
168}
169
170fn expanded(rect: UiRect, amount: f32) -> UiRect {
171 rect.inflate(amount, amount)
172}
173
174fn area(rect: UiRect) -> f64 {
175 rect.width().max(0.0) as f64 * rect.height().max(0.0) as f64
176}