Skip to main content

drmkit_csd/
damage.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Working out how little of a canvas has to be redrawn.
5
6/// One decoration's footprint on the canvas, as of some frame.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8#[non_exhaustive]
9pub struct DamageSlot {
10    /// Whether anything is drawn here at all.
11    pub armed: bool,
12    /// Left edge.
13    pub x: i32,
14    /// Top edge.
15    pub y: i32,
16    /// Width.
17    pub w: u32,
18    /// Height.
19    pub h: u32,
20    /// The surface's content generation.
21    ///
22    /// What separates *moved* from *redrawn*: a decoration at the same place
23    /// whose pixels changed has the same rectangle and a different generation,
24    /// and without this it would look unchanged.
25    pub generation: u64,
26}
27
28/// A rectangle of the canvas that has to be repainted.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30#[allow(
31    clippy::exhaustive_structs,
32    reason = "a value defined by its fields; there is nothing to add to it"
33)]
34pub struct DamageRect {
35    /// Left edge.
36    pub x: i32,
37    /// Top edge.
38    pub y: i32,
39    /// Width.
40    pub w: u32,
41    /// Height.
42    pub h: u32,
43}
44
45impl DamageRect {
46    /// Whether this rectangle covers nothing.
47    #[must_use]
48    pub const fn is_empty(self) -> bool {
49        self.w == 0 || self.h == 0
50    }
51}
52
53/// Accumulates a bounding box.
54#[derive(Default)]
55struct Bounds {
56    valid: bool,
57    x0: i32,
58    y0: i32,
59    x1: i32,
60    y1: i32,
61}
62
63impl Bounds {
64    fn add(&mut self, x: i32, y: i32, w: u32, h: u32) {
65        if w == 0 || h == 0 {
66            return;
67        }
68        let x1 = x.saturating_add(i32::try_from(w).unwrap_or(i32::MAX));
69        let y1 = y.saturating_add(i32::try_from(h).unwrap_or(i32::MAX));
70        if self.valid {
71            self.x0 = self.x0.min(x);
72            self.y0 = self.y0.min(y);
73            self.x1 = self.x1.max(x1);
74            self.y1 = self.y1.max(y1);
75        } else {
76            *self = Self {
77                valid: true,
78                x0: x,
79                y0: y,
80                x1,
81                y1,
82            };
83        }
84    }
85}
86
87/// What changed between two frames' worth of slots.
88///
89/// One rectangle rather than a list: a canvas repaint is a loop over rows, and
90/// the union of two scattered regions is cheaper to redraw whole than to
91/// iterate separately.
92///
93/// **A different number of slots damages everything.** The slots are compared
94/// pairwise by index, so a count change means the pairing itself is
95/// meaningless — comparing slot 2 against what used to be slot 3 would report
96/// changes in the wrong places and miss real ones.
97#[must_use]
98pub fn compute_damage(
99    prev: &[DamageSlot],
100    cur: &[DamageSlot],
101    canvas_w: u32,
102    canvas_h: u32,
103) -> DamageRect {
104    let whole = DamageRect {
105        x: 0,
106        y: 0,
107        w: canvas_w,
108        h: canvas_h,
109    };
110    // Empty `prev` is the first frame: nothing is on the canvas yet, so
111    // everything has to be painted.
112    if prev.len() != cur.len() || prev.is_empty() {
113        return whole;
114    }
115
116    let mut bounds = Bounds::default();
117    for (before, after) in prev.iter().zip(cur) {
118        let changed = before.armed != after.armed
119            || (after.armed
120                && (before.x != after.x
121                    || before.y != after.y
122                    || before.w != after.w
123                    || before.h != after.h
124                    || before.generation != after.generation));
125        if !changed {
126            continue;
127        }
128        // Both footprints, not just the new one: the old has to be vacated or
129        // a decoration that moved leaves a copy of itself behind.
130        if before.armed {
131            bounds.add(before.x, before.y, before.w, before.h);
132        }
133        if after.armed {
134            bounds.add(after.x, after.y, after.w, after.h);
135        }
136    }
137
138    if !bounds.valid {
139        return DamageRect::default();
140    }
141
142    let x0 = bounds.x0.max(0);
143    let y0 = bounds.y0.max(0);
144    let x1 = bounds.x1.min(i32::try_from(canvas_w).unwrap_or(i32::MAX));
145    let y1 = bounds.y1.min(i32::try_from(canvas_h).unwrap_or(i32::MAX));
146    if x1 <= x0 || y1 <= y0 {
147        return DamageRect::default();
148    }
149
150    DamageRect {
151        x: x0,
152        y: y0,
153        w: (x1 - x0).unsigned_abs(),
154        h: (y1 - y0).unsigned_abs(),
155    }
156}
157
158/// The smallest rectangle covering both.
159#[must_use]
160pub fn union_rect(a: DamageRect, b: DamageRect) -> DamageRect {
161    if a.is_empty() {
162        return b;
163    }
164    if b.is_empty() {
165        return a;
166    }
167    let mut bounds = Bounds::default();
168    bounds.add(a.x, a.y, a.w, a.h);
169    bounds.add(b.x, b.y, b.w, b.h);
170    DamageRect {
171        x: bounds.x0,
172        y: bounds.y0,
173        w: (bounds.x1 - bounds.x0).unsigned_abs(),
174        h: (bounds.y1 - bounds.y0).unsigned_abs(),
175    }
176}
177
178/// The overlap between a rectangle and a damage region.
179#[must_use]
180pub fn intersect_rect(x: i32, y: i32, w: u32, h: u32, region: DamageRect) -> DamageRect {
181    let ax1 = x.saturating_add(i32::try_from(w).unwrap_or(i32::MAX));
182    let ay1 = y.saturating_add(i32::try_from(h).unwrap_or(i32::MAX));
183    let bx1 = region
184        .x
185        .saturating_add(i32::try_from(region.w).unwrap_or(i32::MAX));
186    let by1 = region
187        .y
188        .saturating_add(i32::try_from(region.h).unwrap_or(i32::MAX));
189
190    let x0 = x.max(region.x);
191    let y0 = y.max(region.y);
192    let x1 = ax1.min(bx1);
193    let y1 = ay1.min(by1);
194    if x1 <= x0 || y1 <= y0 {
195        return DamageRect::default();
196    }
197    DamageRect {
198        x: x0,
199        y: y0,
200        w: (x1 - x0).unsigned_abs(),
201        h: (y1 - y0).unsigned_abs(),
202    }
203}