1use sequoia_core::{Dirty, Rect};
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum FrameOutcome {
43 Painted,
44 Skipped,
45}
46
47pub(crate) const MAX_DAMAGE_REGIONS: usize = 8;
52
53#[derive(Clone, Debug)]
54pub struct DamageTracker {
55 flags: Dirty,
57 regions: Vec<Rect>,
58 full: bool,
59
60 motion_pending: bool,
62
63 frames_painted: u64,
65 frames_skipped: u64,
66}
67
68impl Default for DamageTracker {
69 fn default() -> Self {
70 Self {
71 flags: Dirty::empty(),
72 regions: Vec::new(),
73 full: false,
74 motion_pending: false,
75 frames_painted: 0,
76 frames_skipped: 0,
77 }
78 }
79}
80
81impl DamageTracker {
82 pub fn new() -> Self {
83 Self::default()
84 }
85
86 pub fn mark_rect(&mut self, rect: Rect) {
92 if rect.is_empty() {
93 return;
94 }
95 self.flags |= Dirty::PAINT;
96 if !self.full {
97 self.add_region(rect);
98 }
99 }
100
101 fn add_region(&mut self, rect: Rect) {
102 let mut merged = rect;
103 let mut i = 0;
104 while i < self.regions.len() {
105 if self.regions[i].intersects(merged) {
106 merged = merged.union(self.regions.remove(i));
107 } else {
108 i += 1;
109 }
110 }
111 self.regions.push(merged);
112 if self.regions.len() > MAX_DAMAGE_REGIONS {
113 let all = self.regions.drain(..).fold(Rect::ZERO, Rect::union);
114 self.regions.push(all);
115 }
116 }
117
118 pub fn mark_full(&mut self) {
120 self.full = true;
121 self.flags |= Dirty::PAINT | Dirty::SURFACE;
122 }
123
124 pub fn mark_layout(&mut self) {
126 self.flags |= Dirty::LAYOUT;
127 }
128
129 pub fn mark(&mut self, flags: Dirty) {
131 self.flags |= flags;
132 }
133
134 pub fn request_frame(&mut self) {
140 self.motion_pending = true;
141 }
142
143 pub fn take_motion(&mut self) -> bool {
145 let was = self.motion_pending;
146 self.motion_pending = false;
147 was
148 }
149
150 pub fn is_empty(&self) -> bool {
158 !self.motion_pending && !self.flags.needs_repaint()
159 }
160
161 pub fn needs_frame(&self) -> bool {
162 !self.is_empty()
163 }
164
165 pub fn needs_layout(&self) -> bool {
166 self.flags.needs_layout()
167 }
168
169 pub fn needs_paint(&self) -> bool {
170 self.flags.needs_repaint()
171 }
172
173 pub fn flags(&self) -> Dirty {
174 self.flags
175 }
176
177 pub fn paint_region(&self) -> Option<Rect> {
187 self.paint_regions()
188 .map(|regions| regions.into_iter().fold(Rect::ZERO, Rect::union))
189 }
190
191 pub fn paint_regions(&self) -> Option<Vec<Rect>> {
198 if self.full || self.flags.needs_layout() || self.regions.is_empty() {
199 None
200 } else {
201 Some(
202 self.regions
203 .iter()
204 .map(|rect| rect.inflate(2.0, 2.0))
205 .collect(),
206 )
207 }
208 }
209
210 pub fn after_paint(&mut self) {
217 self.flags = Dirty::empty();
218 self.regions.clear();
219 self.full = false;
220 self.frames_painted += 1;
221 }
222
223 pub fn record_skipped(&mut self) {
228 self.flags = Dirty::empty();
229 self.regions.clear();
230 self.full = false;
231 self.frames_skipped += 1;
232 }
233
234 pub fn clear_all(&mut self) {
236 self.flags = Dirty::empty();
237 self.regions.clear();
238 self.full = false;
239 self.motion_pending = false;
240 }
241
242 pub fn frames_painted(&self) -> u64 {
247 self.frames_painted
248 }
249
250 pub fn frames_skipped(&self) -> u64 {
251 self.frames_skipped
252 }
253
254 pub fn skip_rate(&self) -> f32 {
256 let total = self.frames_painted + self.frames_skipped;
257 if total == 0 {
258 0.0
259 } else {
260 self.frames_skipped as f32 / total as f32
261 }
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use sequoia_core::Point;
269
270 fn r(x: f32, y: f32, w: f32, h: f32) -> Rect {
271 Rect::from_xywh(x, y, w, h)
272 }
273
274 #[test]
275 fn fresh_tracker_is_empty() {
276 let d = DamageTracker::new();
277 assert!(d.is_empty());
278 assert!(!d.needs_frame());
279 assert!(!d.needs_layout());
280 assert!(!d.needs_paint());
281 assert_eq!(d.paint_region(), None);
282 }
283
284 #[test]
285 fn single_rect_marks_paint_only() {
286 let mut d = DamageTracker::new();
287 d.mark_rect(r(10.0, 10.0, 4.0, 4.0));
288 assert!(!d.is_empty());
289 assert!(!d.needs_layout(), "sam mark_rect nie zmienia layoutu");
290 assert!(d.needs_paint());
291 let region = d.paint_region().unwrap();
292 assert!(region.contains(Point::new(12.0, 12.0)));
293 assert!(region.size.width >= 4.0);
294 }
295
296 #[test]
297 fn disjoint_rects_stay_separate_regions() {
298 let mut d = DamageTracker::new();
299 d.mark_rect(r(0.0, 0.0, 10.0, 10.0));
300 d.mark_rect(r(100.0, 50.0, 4.0, 4.0));
301 let regions = d.paint_regions().unwrap();
302 assert_eq!(regions.len(), 2);
303 assert!(regions[0].right() < regions[1].left());
304 assert!(regions[0].bottom() < regions[1].top());
305
306 let bbox = d.paint_region().unwrap();
307 assert!(bbox.left() <= 0.0);
308 assert!(bbox.right() >= 104.0);
309 assert!(bbox.top() <= 0.0);
310 assert!(bbox.bottom() >= 54.0);
311 }
312
313 #[test]
314 fn overlapping_rects_merge_into_one_region() {
315 let mut d = DamageTracker::new();
316 d.mark_rect(r(0.0, 0.0, 10.0, 10.0));
317 d.mark_rect(r(5.0, 5.0, 10.0, 10.0));
318 let regions = d.paint_regions().unwrap();
319 assert_eq!(regions.len(), 1);
320 assert!(regions[0].contains(Point::new(12.0, 12.0)));
321 }
322
323 #[test]
324 fn too_many_regions_collapse_to_bbox() {
325 let mut d = DamageTracker::new();
326 for i in 0..=MAX_DAMAGE_REGIONS {
327 d.mark_rect(r(i as f32 * 10.0, 0.0, 1.0, 1.0));
328 }
329 let regions = d.paint_regions().unwrap();
330 assert_eq!(regions.len(), 1, "bounded number of scissor passes");
331 assert!(regions[0].right() >= MAX_DAMAGE_REGIONS as f32 * 10.0);
332 }
333
334 #[test]
335 fn empty_rects_are_ignored() {
336 let mut d = DamageTracker::new();
337 d.mark_rect(Rect::ZERO);
338 d.mark_rect(r(5.0, 5.0, 0.0, 0.0));
339 assert!(d.is_empty(), "puste recty nie liczą się");
340 }
341
342 #[test]
343 fn full_wins_over_rects() {
344 let mut d = DamageTracker::new();
345 d.mark_rect(r(0.0, 0.0, 10.0, 10.0));
346 d.mark_full();
347 assert_eq!(d.paint_region(), None, "full = brak partial clip");
348 assert!(d.flags().contains(Dirty::SURFACE));
349 }
350
351 #[test]
352 fn layout_flag_propagates() {
353 let mut d = DamageTracker::new();
354 d.mark_layout();
355 assert!(d.needs_layout());
356 assert!(d.needs_paint(), "layout implies paint");
357 assert_eq!(d.paint_region(), None, "no rect = full paint");
358 }
359
360 #[test]
361 fn layout_with_rect_still_requires_full_paint() {
362 let mut d = DamageTracker::new();
363 d.mark_rect(r(10.0, 10.0, 20.0, 20.0));
364 d.mark_layout();
365 assert_eq!(
366 d.paint_region(),
367 None,
368 "layout changes can move or remove children"
369 );
370 }
371
372 #[test]
373 fn request_frame_forces_next_paint() {
374 let mut d = DamageTracker::new();
375 assert!(!d.needs_frame());
376 d.request_frame();
377 assert!(d.needs_frame(), "motion = ramka do namalowania");
378
379 assert!(d.take_motion());
381 assert!(!d.take_motion(), "drugie take_motion = false");
382 }
383
384 #[test]
385 fn bookkeeping_flags_do_not_request_a_frame() {
386 let mut d = DamageTracker::new();
387 d.mark(Dirty::HIT_TEST | Dirty::DATA);
388 assert!(!d.needs_paint());
389 assert!(!d.needs_frame(), "hit-test/data alone do not paint pixels");
390
391 d.record_skipped();
392 assert_eq!(d.flags(), Dirty::empty());
393 assert!(!d.needs_frame());
394 }
395
396 #[test]
397 fn paint_without_rect_requests_full_repaint() {
398 let mut d = DamageTracker::new();
399 d.mark(Dirty::PAINT);
400 assert!(d.needs_frame());
401 assert_eq!(
402 d.paint_region(),
403 None,
404 "unknown pixel bounds require a full repaint"
405 );
406 }
407
408 #[test]
409 fn after_paint_clears_rects_but_not_motion() {
410 let mut d = DamageTracker::new();
411 d.mark_rect(r(0.0, 0.0, 10.0, 10.0));
412 d.after_paint();
413 assert!(d.is_empty(), "po klatce puste");
414
415 d.request_frame();
417 d.after_paint();
418 assert!(d.needs_frame(), "motion z paint nie ginie");
419 }
420
421 #[test]
422 fn clear_all_resets_everything() {
423 let mut d = DamageTracker::new();
424 d.mark_rect(r(0.0, 0.0, 10.0, 10.0));
425 d.mark_layout();
426 d.request_frame();
427 d.clear_all();
428 assert!(d.is_empty());
429 assert!(!d.needs_frame());
430 assert_eq!(d.paint_region(), None);
431 }
432
433 #[test]
434 fn stats_track_painted_and_skipped() {
435 let mut d = DamageTracker::new();
436 d.mark_rect(r(0.0, 0.0, 1.0, 1.0));
437 d.after_paint();
438 d.record_skipped();
439 d.record_skipped();
440 d.mark_rect(r(0.0, 0.0, 1.0, 1.0));
441 d.after_paint();
442 assert_eq!(d.frames_painted(), 2);
443 assert_eq!(d.frames_skipped(), 2);
444 assert!((d.skip_rate() - 0.5).abs() < 1e-6);
445 }
446
447 #[test]
448 fn many_marks_coalesce_into_one_frame() {
449 let mut d = DamageTracker::new();
450 for i in 0..100 {
451 d.mark_rect(r(i as f32, 0.0, 1.0, 1.0));
452 }
453 d.after_paint();
455 assert_eq!(d.frames_painted(), 1);
456 assert!(d.is_empty());
457 }
458}