Skip to main content

retroglyph_core/grid/
diff.rs

1//! [`Grid::diff`](crate::grid::Grid::diff), the zero-allocation per-cell change iterator [`Terminal::present`](crate::terminal::Terminal::present) and the
2//! software backend build on, plus its per-layer [`LayerDiff`] helper.
3
4use super::{Grid, Pos, flat_index_to_xy};
5use crate::backend::DrawCell;
6#[cfg(test)]
7use crate::color::Style;
8use crate::color::Tint;
9use crate::tile::Tile;
10#[cfg(test)]
11use alloc::vec::Vec;
12
13impl Grid {
14    /// Yield a [`DrawCell`] for every changed position across all layers, in layer-major
15    /// (`0` → `max(self.max_layer, other.max_layer)`) then row-major order.
16    ///
17    /// Four cases per layer:
18    /// - Layer absent in both `self` and `other`: nothing yielded.
19    /// - Layer in `self`, absent in `other` (newly allocated): all `width × height` tiles
20    ///   yielded.
21    /// - Layer in both, and `self` and `other` have matching dimensions: only positions where
22    ///   the `Tile` or its side-table entry (grapheme text, tint) differs are yielded.
23    /// - Layer in both, but `self` and `other` have different dimensions: all positions in
24    ///   `self` are considered changed, same as a newly allocated layer.
25    /// - Layer in `other` but no longer in `self` (stopped being written): every position is
26    ///   yielded as a cleared, default [`Tile`], sized to `other`'s dimensions. This case is
27    ///   only expressible when `self` and `other` have matching dimensions; a simultaneous size
28    ///   change and layer teardown falls back to "nothing yielded" for that layer, matching a
29    ///   layer absent from both.
30    ///
31    /// This iterator is zero-allocation: it walks the layer buffers inline.
32    pub fn diff<'a>(&'a self, other: &'a Self) -> impl Iterator<Item = DrawCell<'a>> + 'a {
33        let width = usize::from(self.width);
34        let max = self.max_layer.max(other.max_layer);
35        let same_size = self.width == other.width && self.height == other.height;
36        (0..=max).flat_map(move |id| {
37            // A size mismatch is treated the same as `other` never having allocated this layer:
38            // `other`'s buffer can't be indexed with `self`'s flat index once the sizes differ,
39            // so every position in `self` is considered changed, matching grixy's `GridDiff`
40            // double-buffering contract.
41            let other_layer = if same_size { other.layer(id) } else { None };
42            match (self.layer(id), other_layer) {
43                // Layer absent in both, or `self` never allocated it while a size mismatch makes
44                // `other`'s buffer unusable for positions: nothing changed.
45                (None, None) => LayerDiff::Empty,
46                // Layer stopped being written: `self` no longer has it, but `other` (same
47                // dimensions, checked by `other_layer` above) still does. Report every position
48                // as cleared so a compositing backend can retire the layer instead of continuing
49                // to show its stale content; see retroglyph#1018.
50                (None, Some(prev_lb)) => LayerDiff::Cleared(
51                    prev_lb.buf.as_ref().iter().enumerate().map(move |(i, _)| {
52                        let (x, y) = flat_index_to_xy(i, width);
53                        DrawCell {
54                            layer: id,
55                            pos: Pos::new(x, y),
56                            tile: &Tile::EMPTY,
57                            grapheme: None,
58                            tint: Tint::None,
59                        }
60                    }),
61                ),
62                // Newly allocated layer: all cells are "changed".
63                (Some(cur_lb), None) => LayerDiff::Full(
64                    cur_lb
65                        .buf
66                        .as_ref()
67                        .iter()
68                        .enumerate()
69                        .map(move |(i, tile)| {
70                            let (x, y) = flat_index_to_xy(i, width);
71                            DrawCell {
72                                layer: id,
73                                pos: Pos::new(x, y),
74                                tile,
75                                grapheme: cur_lb.extra_for(i, tile),
76                                tint: cur_lb.tint_for(i, tile),
77                            }
78                        }),
79                ),
80                // Layer in both: only the differing cells. Compared by hand
81                // (rather than delegating to grixy's `GridDiff`) because a
82                // `Tile`-only comparison can't see grapheme-text changes: two
83                // multi-codepoint EGCs sharing a primary codepoint but
84                // different combining marks (e.g. `e\u{0301}` vs `e\u{0300}`)
85                // compare equal on every `Tile` field.
86                (Some(cur_lb), Some(prev_lb)) => {
87                    LayerDiff::Diff(cur_lb.buf.as_ref().iter().enumerate().filter_map(
88                        move |(i, tile)| {
89                            let prev_tile = &prev_lb.buf.as_ref()[i];
90                            // The whole entry, not just its grapheme: a `Tile`-only comparison
91                            // cannot see a change to either member of the side table, and a
92                            // tint-only change is as real a redraw as a combining-mark change.
93                            let cur_extra = cur_lb.entry_for(i, tile);
94                            let prev_extra = prev_lb.entry_for(i, prev_tile);
95                            if tile == prev_tile && cur_extra == prev_extra {
96                                return None;
97                            }
98                            let (x, y) = flat_index_to_xy(i, width);
99                            Some(DrawCell {
100                                layer: id,
101                                pos: Pos::new(x, y),
102                                tile,
103                                grapheme: cur_extra.and_then(|e| e.grapheme.as_deref()),
104                                tint: cur_extra.map_or(Tint::None, |e| e.tint),
105                            })
106                        },
107                    ))
108                }
109            }
110        })
111    }
112}
113
114/// Per-layer diff iterator, replacing a boxed trait object so `diff` performs
115/// no per-layer heap allocation.
116enum LayerDiff<F, D, C> {
117    Empty,
118    Full(F),
119    Diff(D),
120    Cleared(C),
121}
122
123impl<'a, F, D, C> Iterator for LayerDiff<F, D, C>
124where
125    F: Iterator<Item = DrawCell<'a>>,
126    D: Iterator<Item = DrawCell<'a>>,
127    C: Iterator<Item = DrawCell<'a>>,
128{
129    type Item = DrawCell<'a>;
130
131    fn next(&mut self) -> Option<Self::Item> {
132        match self {
133            Self::Empty => None,
134            Self::Full(iter) => iter.next(),
135            Self::Diff(iter) => iter.next(),
136            Self::Cleared(iter) => iter.next(),
137        }
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn diff_reports_a_written_cell_as_a_draw_cell() {
147        let mut g1 = Grid::new(2, 2);
148        let g2 = Grid::new(2, 2);
149
150        g1.put_tile(0, (0, 0), Tile::default().with_glyph('A'));
151
152        let diffs: Vec<_> = g1.diff(&g2).collect();
153        assert_eq!(diffs.len(), 1);
154        assert_eq!(
155            diffs[0],
156            DrawCell::on_layer(0, Pos::new(0, 0), &g1[Pos::new(0, 0)])
157        );
158    }
159
160    #[test]
161    fn diff_empty_when_identical() {
162        let g = Grid::new(5, 5);
163        let prev = Grid::new(5, 5);
164        assert_eq!(g.diff(&prev).count(), 0);
165    }
166
167    #[test]
168    fn diff_reports_changed_cell() {
169        let mut cur = Grid::new(5, 5);
170        let prev = Grid::new(5, 5);
171        cur.put_tile(0, (2, 3), Tile::new('X', Style::default()));
172        let diffs: Vec<_> = cur.diff(&prev).collect();
173        assert_eq!(diffs.len(), 1);
174        assert_eq!(diffs[0].layer, 0);
175        assert_eq!(diffs[0].pos, Pos::new(2, 3));
176        assert_eq!(diffs[0].tile.glyph, 'X');
177    }
178
179    #[test]
180    fn diff_new_layer_yields_all_cells() {
181        let mut cur = Grid::new(3, 4);
182        let prev = Grid::new(3, 4);
183        cur.put_tile(1, (0, 0), Tile::new('A', Style::default()));
184        let diffs: Vec<_> = cur.diff(&prev).collect();
185        // All 12 cells of the newly allocated layer 1 are yielded.
186        assert_eq!(diffs.len(), 12);
187        assert!(diffs.iter().all(|c| c.layer == 1));
188    }
189
190    #[test]
191    fn diff_mismatched_sizes_yields_full_diff() {
192        // A smaller `other` must not panic; every cell in `self` is reported as changed instead.
193        let mut cur = Grid::new(3, 2);
194        let prev = Grid::new(2, 2);
195        cur.put_tile(0, (0, 0), Tile::new('X', Style::default()));
196        let diffs: Vec<_> = cur.diff(&prev).collect();
197        assert_eq!(diffs.len(), 6);
198        assert!(diffs.iter().all(|c| c.layer == 0));
199    }
200
201    #[test]
202    fn diff_layer_major_order() {
203        let mut cur = Grid::new(3, 3);
204        let prev = Grid::new(3, 3);
205        cur.put_tile(2, (0, 0), Tile::new('B', Style::default()));
206        cur.put_tile(0, (1, 0), Tile::new('A', Style::default()));
207        let layers: Vec<u8> = cur.diff(&prev).map(|c| c.layer).collect();
208        // Layer 0's change appears first, then all of layer 2.
209        assert_eq!(layers[0], 0);
210        assert!(layers[1..].iter().all(|&l| l == 2));
211    }
212
213    #[test]
214    fn diff_reports_layer_that_stopped_being_written() {
215        // frame 1: a HUD is drawn on layer 5.
216        let mut a = Grid::new(4, 4);
217        a.put_tile(5, (0, 0), Tile::new('H', Style::default()));
218
219        // frame 2: the HUD is hidden, so `b` never allocates layer 5.
220        let b = Grid::new(4, 4);
221
222        // `b.max_layer()` is 0, but layer 5's stale content in `a` must still be reported so a
223        // compositing backend can clear it instead of continuing to show it (retroglyph#1018).
224        let diffs: Vec<_> = b.diff(&a).collect();
225        assert_eq!(diffs.len(), 16);
226        assert!(diffs.iter().all(|c| c.layer == 5));
227        assert!(diffs.iter().all(|c| c.tile.glyph == ' '));
228        assert!(diffs.iter().all(|c| c.grapheme.is_none()));
229    }
230
231    #[test]
232    fn diff_stopped_layer_with_size_mismatch_yields_nothing_for_that_layer() {
233        // A layer that stopped being written *and* a size change happening at once can't be
234        // expressed against `other`'s buffer, so it falls back to "nothing yielded", same as a
235        // layer absent from both sides.
236        let mut a = Grid::new(4, 4);
237        a.put_tile(5, (0, 0), Tile::new('H', Style::default()));
238        let b = Grid::new(3, 3);
239
240        let diffs: Vec<_> = b.diff(&a).collect();
241        assert!(diffs.iter().all(|c| c.layer != 5));
242    }
243
244    #[cfg(feature = "egc")]
245    #[test]
246    fn diff_detects_grapheme_only_change() {
247        // Same glyph, style, and flags on both sides: only the combining
248        // mark differs. A `Tile`-only diff would miss this.
249        let mut cur = Grid::new(2, 2);
250        let mut prev = Grid::new(2, 2);
251        cur.write_grapheme(0, 0, 0, "e\u{0301}", Style::default());
252        prev.write_grapheme(0, 0, 0, "e\u{0300}", Style::default());
253
254        let diffs: Vec<_> = cur.diff(&prev).collect();
255        assert_eq!(diffs.len(), 1);
256        assert_eq!(diffs[0].pos, Pos::new(0, 0));
257        assert_eq!(diffs[0].grapheme, Some("e\u{0301}"));
258
259        // Identical grapheme text on both sides: no diff.
260        let mut prev2 = Grid::new(2, 2);
261        prev2.write_grapheme(0, 0, 0, "e\u{0301}", Style::default());
262        assert_eq!(cur.diff(&prev2).count(), 0);
263    }
264
265    #[test]
266    fn diff_detects_tint_only_change() {
267        // Same glyph, style, and flags on both sides: only the tint (the side table's other
268        // member) differs. A `Tile`-only diff would miss this too, same as the grapheme case.
269        let mut cur = Grid::new(2, 2);
270        let mut prev = Grid::new(2, 2);
271        cur.put_tile(0, (0, 0), Tile::new('@', Style::default()));
272        prev.put_tile(0, (0, 0), Tile::new('@', Style::default()));
273        cur.set_tint(0, 0, 0, Tint::multiply(64, 128, 192));
274
275        let diffs: Vec<_> = cur.diff(&prev).collect();
276        assert_eq!(diffs.len(), 1);
277        assert_eq!(diffs[0].pos, Pos::new(0, 0));
278        assert_eq!(diffs[0].tint, Tint::multiply(64, 128, 192));
279
280        // Identical tint on both sides: no diff.
281        let mut prev2 = Grid::new(2, 2);
282        prev2.put_tile(0, (0, 0), Tile::new('@', Style::default()));
283        prev2.set_tint(0, 0, 0, Tint::multiply(64, 128, 192));
284        assert_eq!(cur.diff(&prev2).count(), 0);
285    }
286}