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}