Skip to main content

katui_present/surface/
patched.rs

1//! The patched kitty strategy: one whole transmit, then per-frame
2//! retransmits of only the tiles whose pixels changed, each under a
3//! stable tile image id whose placement it replaces.
4
5use crate::Sink;
6use std::collections::HashMap;
7use std::io::{self, Write as _};
8
9use katui_proto::kitty::{
10    Delete, IdSpace, ImageId as KittyImage, Place, PlacementId as KittyPlacement,
11};
12use katui_proto::placeholder::placeholder_row;
13use katui_proto::probe::CellPixels;
14use katui_scene::cell::Position;
15use katui_scene::placement::CellRect;
16use katui_scene::surface::{PixelRect, Surface, diff_rects};
17
18use crate::anchor::Anchor;
19
20use super::{Stash, damage_is_dense, transmit_pixels, write_whole_image};
21use crate::cells::move_cursor;
22use crate::kitty::crop_rect;
23
24/// The patched kitty surface presenter: transmits the whole surface once,
25/// then per frame retransmits only the tiles whose pixels changed, each
26/// as its own small image under a stable tile id, placed over the base.
27/// The kitty protocol has no in-place pixel update (retransmitting an id
28/// replaces the image and its placements), so a tile id is simply
29/// retransmitted and its placement replaced without flicker. A frame
30/// with more damaged tiles than the patch budget, a resize, or anything
31/// that loses the base image falls back to one whole retransmit.
32/// An unchanged frame writes nothing.
33///
34/// The default tile side is `cell.width * cell.height` pixels, a
35/// multiple of both cell axes, so every tile maps to whole cells and
36/// patch edges never tear.
37#[derive(Debug)]
38pub struct PatchedKitty {
39    base: u32,
40    tile_side: u32,
41    budget: usize,
42    prev: Option<Surface>,
43    base_shown: Option<KittyImage>,
44    tile_generations: HashMap<u32, bool>,
45    grid: Grid,
46    cell: CellPixels,
47    stash: Option<Box<dyn Stash>>,
48    repaint: bool,
49}
50
51/// Where tiles go under a multiplexer: nowhere special outside one, on a
52/// placeholder anchor in kitty, or each under its own placeholder text.
53#[derive(Debug)]
54enum Grid {
55    Plain,
56    Anchored(Anchor),
57    Placeholders,
58}
59
60impl PatchedKitty {
61    /// A presenter for surfaces drawn at `cell` pixel resolution, handing
62    /// out image ids from `base` upward, tiling at the cell-derived side
63    /// with the sprite-friendly budget of 64 tiles.
64    #[must_use]
65    pub fn new(base: u32, cell: CellPixels) -> Self {
66        Self::with_tiles(
67            base,
68            cell,
69            u32::from(cell.width) * u32::from(cell.height),
70            64,
71        )
72    }
73
74    /// A presenter with an explicit `tile_side` in surface pixels and
75    /// `budget`, the damaged-tile count past which one whole retransmit
76    /// wins. Fine-grained surfaces want small tiles and a high budget;
77    /// the tile's cell span always follows the surface-to-area scale.
78    #[must_use]
79    pub fn with_tiles(base: u32, cell: CellPixels, tile_side: u32, budget: usize) -> Self {
80        Self {
81            base,
82            tile_side: tile_side.max(1),
83            budget,
84            prev: None,
85            base_shown: None,
86            tile_generations: HashMap::new(),
87            grid: Grid::Plain,
88            cell,
89            stash: None,
90            repaint: false,
91        }
92    }
93
94    /// Send pixels through `stash`, for a terminal on this machine that
95    /// reads them from there.
96    #[must_use]
97    pub fn with_stash(mut self, stash: Box<dyn Stash>) -> Self {
98        self.stash = Some(stash);
99        self
100    }
101
102    /// Draw placeholder tiles for cells of `cell` pixels from now on, after
103    /// a font zoom changed them.
104    pub fn set_cell(&mut self, cell: CellPixels) {
105        self.cell = cell;
106    }
107
108    /// Send every pixel again on the next frame, for when the terminal
109    /// may have lost what it was showing.
110    pub fn repaint(&mut self) {
111        self.repaint = true;
112    }
113
114    /// Free every image this presenter sent and repaint on the next frame.
115    pub fn free<W: Sink>(&mut self, out: &mut W) -> io::Result<()> {
116        let mut bytes = Vec::new();
117        self.free_tiles(&mut bytes)?;
118        for image in [self.base, self.base + 1] {
119            bytes.write_all(&Delete::Free(KittyImage(image)).encode())?;
120        }
121        self.base_shown = None;
122        self.repaint = true;
123        out.write_all(&bytes)
124    }
125
126    /// Send every frame as small tiles for a multiplexer, which drops
127    /// uploads bigger than it buffers and hides placeholder text with its
128    /// pane. With `relative_placements` every tile sits on one placeholder
129    /// anchor and stretches to its cells; without, each tile shows through
130    /// placeholder text of its own, which keeps its aspect and so needs
131    /// the true cell size.
132    #[must_use]
133    pub fn for_multiplexer(mut self, space: IdSpace, relative_placements: bool) -> Self {
134        self.base = space.image(self.base).0;
135        self.grid = if relative_placements {
136            Grid::Anchored(Anchor::new(space))
137        } else {
138            Grid::Placeholders
139        };
140        self
141    }
142
143    /// Write one frame into `area` cells and return the bytes written.
144    pub fn render_frame<W: Sink>(
145        &mut self,
146        out: &mut W,
147        surface: &Surface,
148        area: CellRect,
149    ) -> io::Result<usize> {
150        let resized = self.prev.as_ref().is_none_or(|prev| {
151            prev.width() != surface.width() || prev.height() != surface.height()
152        });
153        let repaint = std::mem::take(&mut self.repaint);
154        if !matches!(self.grid, Grid::Plain) {
155            if resized && matches!(self.grid, Grid::Placeholders) {
156                self.fit_tiles_to_cells(surface, area);
157            }
158            return self.render_tiles(out, surface, area, resized, repaint);
159        }
160        let fresh_start = resized || repaint;
161        let damage = match self.prev.as_ref() {
162            Some(prev) if !fresh_start => self.damage(prev, surface, area),
163            _ => Vec::new(),
164        };
165        let dense = damage_is_dense(surface, self.tile_side, damage.len());
166        let patching = !fresh_start && !dense && damage.len() <= self.budget;
167
168        let mut bytes = Vec::new();
169        if !patching {
170            self.full_frame(&mut bytes, surface, area)?;
171        } else if damage.is_empty() {
172            self.prev = Some(surface.clone());
173            return Ok(0);
174        } else {
175            self.patch_all(&mut bytes, surface, &damage, area)?;
176        }
177        self.prev = Some(surface.clone());
178        let written = bytes.len();
179        out.write_all(&bytes)?;
180        Ok(written)
181    }
182
183    /// Make the tile side a whole number of cells again when a resize or a
184    /// font zoom changed the cell. A placeholder tile covers whole cells,
185    /// so a tile that cut a cell in two would leave cells uncovered.
186    fn fit_tiles_to_cells(&mut self, surface: &Surface, area: CellRect) {
187        let width = surface.width().checked_div(u32::from(area.columns));
188        let height = surface.height().checked_div(u32::from(area.rows));
189        if let (Some(width), Some(height)) = (width, height)
190            && width > 0
191            && height > 0
192            && width * u32::from(area.columns) == surface.width()
193            && height * u32::from(area.rows) == surface.height()
194            && (!self.tile_side.is_multiple_of(width) || !self.tile_side.is_multiple_of(height))
195        {
196            let whole = least_common_multiple(width, height);
197            self.tile_side = whole * SMALLEST_TILE.div_ceil(whole);
198        }
199    }
200
201    /// Every damaged rectangle as one synchronized update.
202    fn patch_all(
203        &mut self,
204        bytes: &mut Vec<u8>,
205        surface: &Surface,
206        damage: &[PixelRect],
207        area: CellRect,
208    ) -> io::Result<()> {
209        bytes.write_all(b"\x1b[?2026h")?;
210        let mut shows = Vec::new();
211        for rect in damage {
212            self.patch(bytes, &mut shows, surface, *rect, area)?;
213        }
214        bytes.append(&mut shows);
215        bytes.write_all(b"\x1b[?2026l")
216    }
217
218    /// A repaint keeps the old tiles up until each one is replaced. Every
219    /// upload goes out before any tile shows, because a multiplexer gives
220    /// us no synchronized update: the new frame then appears at once
221    /// instead of in strips while the uploads trickle through.
222    fn render_tiles<W: Sink>(
223        &mut self,
224        out: &mut W,
225        surface: &Surface,
226        area: CellRect,
227        resized: bool,
228        repaint: bool,
229    ) -> io::Result<usize> {
230        let blank = Surface::new(surface.width(), surface.height());
231        let before = self
232            .prev
233            .as_ref()
234            .filter(|_| !resized && !repaint)
235            .unwrap_or(&blank);
236        let damage = self.damage(before, surface, area);
237        let mut bytes = Vec::new();
238        if resized {
239            self.free_tiles(&mut bytes)?;
240        }
241        if !damage.is_empty()
242            && let Grid::Anchored(anchor) = &mut self.grid
243        {
244            anchor.write(&mut bytes)?;
245        }
246        let mut shows = Vec::new();
247        for rect in &damage {
248            self.patch(&mut bytes, &mut shows, surface, *rect, area)?;
249        }
250        bytes.append(&mut shows);
251        self.prev = Some(surface.clone());
252        out.write_all(&bytes)?;
253        Ok(bytes.len())
254    }
255
256    /// Ghostty's range delete frees every image, the base included, so we
257    /// free the shown tiles one by one.
258    fn free_tiles(&mut self, bytes: &mut Vec<u8>) -> io::Result<()> {
259        for (index, generation) in std::mem::take(&mut self.tile_generations) {
260            bytes.write_all(&Delete::Free(self.tile_image(index, generation)).encode())?;
261        }
262        Ok(())
263    }
264
265    /// The whole surface under the base id not on screen, so the old
266    /// image stays visible until the new one is placed: multiplexers
267    /// drop synchronized updates, and retransmitting a visible id
268    /// removes its placement for a moment.
269    fn full_frame(
270        &mut self,
271        bytes: &mut Vec<u8>,
272        surface: &Surface,
273        area: CellRect,
274    ) -> io::Result<()> {
275        let image = if self.base_shown == Some(KittyImage(self.base)) {
276            KittyImage(self.base + 1)
277        } else {
278            KittyImage(self.base)
279        };
280        bytes.write_all(b"\x1b[?2026h")?;
281        write_whole_image(bytes, image, surface, area, self.stash.as_deref_mut())?;
282        // old tiles sit above the base and would hide the new pixels
283        self.free_tiles(bytes)?;
284        if let Some(shown) = self.base_shown.replace(image) {
285            bytes.write_all(&Delete::Free(shown).encode())?;
286        }
287        bytes.write_all(b"\x1b[?2026l")?;
288        Ok(())
289    }
290
291    /// The damaged tiles since `before`.
292    fn damage(&self, before: &Surface, surface: &Surface, area: CellRect) -> Vec<PixelRect> {
293        diff_rects(before, surface, cell_tiles(surface, area, self.tile_side))
294    }
295
296    fn tile_image(&self, index: u32, second_generation: bool) -> KittyImage {
297        KittyImage(self.base + 2 + index * 2 + u32::from(second_generation))
298    }
299
300    /// One damaged tile as its own small image at the tile's cell
301    /// rectangle: its upload into `uploads`, and into `reveal` what puts
302    /// it on screen and frees the image it replaces. Each tile alternates
303    /// between two ids so its previous pixels stay visible until the new
304    /// ones are placed.
305    fn patch(
306        &mut self,
307        uploads: &mut Vec<u8>,
308        reveal: &mut Vec<u8>,
309        surface: &Surface,
310        rect: PixelRect,
311        area: CellRect,
312    ) -> io::Result<()> {
313        let (left, right) = column_span(rect, surface, area);
314        let (top, bottom) = row_span(rect, surface, area);
315        let columns = right.saturating_sub(left).max(1);
316        let rows = bottom.saturating_sub(top).max(1);
317        let origin_x = area.x.saturating_add(left);
318        let origin_y = area.y.saturating_add(top);
319        let index = tile_index(origin_x, origin_y);
320        let shown = self.tile_generations.get(&index).copied();
321        let generation = shown.is_some_and(|generation| !generation);
322        let image = self.tile_image(index, generation);
323        let origin = Position {
324            x: origin_x,
325            y: origin_y,
326        };
327        let cells = (columns, rows);
328        if matches!(self.grid, Grid::Placeholders) {
329            upload_placeholder_tile(uploads, image, surface, rect, cells, self.cell)?;
330            show_placeholder_tile(reveal, image, origin, cells)?;
331        } else {
332            let padded = with_apron(rect, surface);
333            transmit_pixels(
334                uploads,
335                image,
336                &tile_pixels(surface, padded),
337                (padded.width, padded.height),
338                self.stash.as_deref_mut(),
339            )?;
340            self.place_tile(reveal, image, rect, padded, origin, cells)?;
341        }
342        if let Some(previous) = shown {
343            reveal.write_all(&Delete::Free(self.tile_image(index, previous)).encode())?;
344        }
345        self.tile_generations.insert(index, generation);
346        Ok(())
347    }
348
349    /// A tile placed over the base at its cell rectangle, cropped out of
350    /// the `padded` copy with an apron around it.
351    fn place_tile(
352        &self,
353        bytes: &mut Vec<u8>,
354        image: KittyImage,
355        rect: PixelRect,
356        padded: PixelRect,
357        origin: Position,
358        cells: (u16, u16),
359    ) -> io::Result<()> {
360        let (columns, rows) = cells;
361        let index = tile_index(origin.x, origin.y);
362        let mut place = Place::new(image);
363        place.columns = u32::from(columns);
364        place.rows = u32::from(rows);
365        place.placement = KittyPlacement(2 + index);
366        place.crop = Some(crop_rect(PixelRect {
367            x: rect.x - padded.x,
368            y: rect.y - padded.y,
369            width: rect.width,
370            height: rect.height,
371        }));
372        if let Grid::Anchored(anchor) = &self.grid {
373            place.parent = Some(anchor.offset_to(origin));
374        } else {
375            move_cursor(bytes, origin)?;
376        }
377        bytes.write_all(&place.encode())
378    }
379}
380
381/// The tile whose top left cell is `column`, `row`.
382fn tile_index(column: u16, row: u16) -> u32 {
383    u32::from(column) + u32::from(row) * 4096
384}
385
386/// The surface cut into tiles about `tile_side` pixels square, on cell
387/// edges: a surface at any resolution then has every tile cover whole
388/// cells, and a tile placed on its cells lines up with the base image.
389fn cell_tiles(surface: &Surface, area: CellRect, tile_side: u32) -> Vec<PixelRect> {
390    let columns = u32::from(area.columns).max(1);
391    let rows = u32::from(area.rows).max(1);
392    let across = cells_per_tile(tile_side, surface.width(), columns);
393    let down = cells_per_tile(tile_side, surface.height(), rows);
394    let mut tiles = Vec::new();
395    for row in (0..rows).step_by(down as usize) {
396        let top = edge(row, surface.height(), rows);
397        let bottom = edge((row + down).min(rows), surface.height(), rows);
398        for column in (0..columns).step_by(across as usize) {
399            let left = edge(column, surface.width(), columns);
400            let right = edge((column + across).min(columns), surface.width(), columns);
401            if right > left && bottom > top {
402                tiles.push(PixelRect {
403                    x: left,
404                    y: top,
405                    width: right - left,
406                    height: bottom - top,
407                });
408            }
409        }
410    }
411    tiles
412}
413
414/// How many of `cells` cells spanning `pixels` pixels make a tile about
415/// `side` pixels long.
416fn cells_per_tile(side: u32, pixels: u32, cells: u32) -> u32 {
417    let per_tile = (u64::from(side) * u64::from(cells) * 2 + u64::from(pixels))
418        / (2 * u64::from(pixels.max(1)));
419    u32::try_from(per_tile).unwrap_or(cells).clamp(1, cells)
420}
421
422/// The pixel nearest where cell `cell` starts, with `cells` cells
423/// spanning `pixels` pixels.
424fn edge(cell: u32, pixels: u32, cells: u32) -> u32 {
425    let at = (u64::from(cell) * u64::from(pixels) * 2 + u64::from(cells))
426        / (2 * u64::from(cells.max(1)));
427    u32::try_from(at).unwrap_or(pixels)
428}
429
430/// The cell edge nearest pixel `pixel`, the inverse of [`edge`].
431fn cell_at(pixel: u32, pixels: u32, cells: u32) -> u16 {
432    let at = (u64::from(pixel) * u64::from(cells) * 2 + u64::from(pixels))
433        / (2 * u64::from(pixels.max(1)));
434    u16::try_from(at).unwrap_or(u16::MAX)
435}
436
437/// The first and past-the-end columns `rect` covers.
438fn column_span(rect: PixelRect, surface: &Surface, area: CellRect) -> (u16, u16) {
439    let columns = u32::from(area.columns);
440    (
441        cell_at(rect.x, surface.width(), columns),
442        cell_at(rect.x + rect.width, surface.width(), columns),
443    )
444}
445
446/// The first and past-the-end rows `rect` covers.
447fn row_span(rect: PixelRect, surface: &Surface, area: CellRect) -> (u16, u16) {
448    let rows = u32::from(area.rows);
449    (
450        cell_at(rect.y, surface.height(), rows),
451        cell_at(rect.y + rect.height, surface.height(), rows),
452    )
453}
454
455/// The smallest tile side in pixels we refit to, so a whole frame stays a
456/// few hundred tiles.
457const SMALLEST_TILE: u32 = 64;
458
459fn least_common_multiple(first: u32, second: u32) -> u32 {
460    let (mut a, mut b) = (first, second);
461    while b != 0 {
462        (a, b) = (b, a % b);
463    }
464    first / a.max(1) * second
465}
466
467/// A terminal fits a placeholder image into its cells keeping its aspect,
468/// so we stretch the tile's height to the aspect of its cells and keep its
469/// width, which keeps a game's reduced resolution.
470fn upload_placeholder_tile(
471    bytes: &mut Vec<u8>,
472    image: KittyImage,
473    surface: &Surface,
474    rect: PixelRect,
475    (columns, rows): (u16, u16),
476    cell: CellPixels,
477) -> io::Result<()> {
478    let cells_wide = (u32::from(columns) * u32::from(cell.width)).max(1);
479    let cells_high = u32::from(rows) * u32::from(cell.height);
480    let width = rect.width.max(1);
481    let height = (u64::from(width) * u64::from(cells_high) / u64::from(cells_wide)).max(1);
482    let height = u32::try_from(height).unwrap_or(rect.height);
483    let pixels = resample(
484        &tile_pixels(surface, rect),
485        (rect.width, rect.height),
486        (width, height),
487    );
488    transmit_pixels(bytes, image, &pixels, (width, height), None)
489}
490
491/// A virtual placement of the tile's cell size, and the placeholder text
492/// that shows it written over its cells.
493fn show_placeholder_tile(
494    bytes: &mut Vec<u8>,
495    image: KittyImage,
496    origin: Position,
497    (columns, rows): (u16, u16),
498) -> io::Result<()> {
499    let mut place = Place::new(image);
500    place.columns = u32::from(columns);
501    place.rows = u32::from(rows);
502    place.virtual_placement = true;
503    bytes.write_all(&place.encode())?;
504    for row in 0..rows {
505        if let Some(text) = placeholder_row(image, u32::from(row), u32::from(columns)) {
506            move_cursor(
507                bytes,
508                Position {
509                    x: origin.x,
510                    y: origin.y.saturating_add(row),
511                },
512            )?;
513            bytes.write_all(text.as_bytes())?;
514        }
515    }
516    Ok(())
517}
518
519/// `pixels` of `from` size scaled to `to` by nearest neighbour, or as they
520/// are when the sizes match.
521fn resample(pixels: &[u8], from: (u32, u32), to: (u32, u32)) -> Vec<u8> {
522    if from == to || from.0 == 0 || from.1 == 0 {
523        return pixels.to_vec();
524    }
525    let mut scaled = Vec::with_capacity((to.0 * to.1 * 4) as usize);
526    for y in 0..to.1 {
527        let source_row = (y * from.1 / to.1) * from.0;
528        for x in 0..to.0 {
529            let start = ((source_row + x * from.0 / to.0) * 4) as usize;
530            scaled.extend_from_slice(pixels.get(start..start + 4).unwrap_or(&[0; 4]));
531        }
532    }
533    scaled
534}
535
536/// `rect` grown by one pixel on every side the surface has room for. A
537/// terminal filters each image on its own, so we send a tile with its
538/// neighbours' edge pixels and crop them away, which blends tile edges the
539/// way one whole image would.
540fn with_apron(rect: PixelRect, surface: &Surface) -> PixelRect {
541    let left = rect.x.saturating_sub(1);
542    let top = rect.y.saturating_sub(1);
543    let right = (rect.x + rect.width + 1).min(surface.width());
544    let bottom = (rect.y + rect.height + 1).min(surface.height());
545    PixelRect {
546        x: left,
547        y: top,
548        width: right - left,
549        height: bottom - top,
550    }
551}
552
553/// The tile's RGBA rows out of the surface, row-major, tile width per
554/// row: exactly the payload a tile transmit declares.
555pub(super) fn tile_pixels(surface: &Surface, rect: PixelRect) -> Vec<u8> {
556    let row_bytes = (surface.width() * 4) as usize;
557    let mut tile = Vec::with_capacity((rect.width * rect.height * 4) as usize);
558    for (row_index, row) in surface.pixels().chunks(row_bytes).enumerate() {
559        if (row_index as u32) < rect.y || (row_index as u32) >= rect.y + rect.height {
560            continue;
561        }
562        let start = (rect.x * 4) as usize;
563        let end = start + (rect.width * 4) as usize;
564        if let Some(slice) = row.get(start..end) {
565            tile.extend_from_slice(slice);
566        }
567    }
568    tile
569}
570
571#[cfg(test)]
572mod tests {
573    use super::PatchedKitty;
574
575    fn plain_surface(width: u32, height: u32) -> Surface {
576        crate::surface::test_support::solid(width, height, Rgba::solid(10, 10, 30))
577    }
578    use katui_proto::probe::CellPixels;
579    use katui_scene::placement::CellRect;
580    use katui_scene::surface::{Rgba, Surface};
581
582    // 8x8 px tiles with a 2x4 cell: 2*4=8, a multiple of both axes
583    fn cell() -> CellPixels {
584        CellPixels {
585            width: 2,
586            height: 4,
587        }
588    }
589
590    #[test]
591    fn the_first_frame_transmits_the_whole_surface() {
592        let mut presenter = PatchedKitty::new(90, cell());
593        let mut out = Vec::new();
594        presenter
595            .render_frame(
596                &mut out,
597                &plain_surface(64, 64),
598                crate::surface::test_support::area(),
599            )
600            .expect("writing to a Vec cannot fail");
601        let text = String::from_utf8_lossy(&out);
602        assert!(text.contains("a=t"), "the whole image travels");
603        assert!(text.contains("o=z"), "compressed");
604        assert!(text.contains("a=p,i=90"), "and is placed");
605    }
606
607    #[test]
608    fn an_identical_frame_writes_nothing() {
609        let mut presenter = PatchedKitty::new(90, cell());
610        let surface = plain_surface(64, 64);
611        let mut first = Vec::new();
612        presenter
613            .render_frame(&mut first, &surface, crate::surface::test_support::area())
614            .expect("writing to a Vec cannot fail");
615        let mut second = Vec::new();
616        let written = presenter
617            .render_frame(&mut second, &surface, crate::surface::test_support::area())
618            .expect("writing to a Vec cannot fail");
619        assert_eq!(written, 0, "no damage means no bytes");
620        assert!(second.is_empty());
621    }
622
623    #[test]
624    fn tiles_of_a_scaled_surface_cover_whole_cells_edge_to_edge() {
625        let surface = plain_surface(50, 37);
626        let area = CellRect::full(16, 9);
627        let tiles = super::cell_tiles(&surface, area, 8);
628        let covered: u32 = tiles.iter().map(|tile| tile.width * tile.height).sum();
629        assert_eq!(covered, 50 * 37, "the tiles cover the surface once");
630        let mut next_column = 0;
631        for tile in tiles.iter().filter(|tile| tile.y == 0) {
632            let (left, right) = super::column_span(*tile, &surface, area);
633            assert_eq!(left, next_column, "each tile starts where the last ended");
634            assert!(right > left, "every tile covers a cell");
635            next_column = right;
636        }
637        assert_eq!(next_column, 16, "the last tile ends at the last column");
638    }
639
640    #[test]
641    fn a_small_change_retransmits_only_its_tile() {
642        let mut presenter = PatchedKitty::new(90, cell());
643        let mut surface = plain_surface(64, 64);
644        let mut warmup = Vec::new();
645        presenter
646            .render_frame(&mut warmup, &surface, crate::surface::test_support::area())
647            .expect("writing to a Vec cannot fail");
648        surface.set(
649            katui_scene::surface::PixelPos::new(40, 40),
650            Rgba::solid(255, 255, 255),
651        );
652        let mut out = Vec::new();
653        presenter
654            .render_frame(&mut out, &surface, crate::surface::test_support::area())
655            .expect("writing to a Vec cannot fail");
656        let text = String::from_utf8_lossy(&out);
657        assert!(
658            !text.contains("s=64"),
659            "the whole surface does not travel again"
660        );
661        assert!(
662            text.contains("s=10,v=10"),
663            "the damaged 8x8 tile travels with a pixel of apron on each side"
664        );
665        assert!(
666            text.contains("x=1,y=1,w=8,h=8"),
667            "the placement crops the apron away"
668        );
669        assert!(
670            text.contains("a=p,i=82032") && text.contains("c=2,r=2"),
671            "the tile covers its 2x2 cells at the surface's scale"
672        );
673    }
674
675    #[test]
676    fn a_whole_frame_frees_the_tiles_that_would_cover_it() {
677        let mut presenter = PatchedKitty::new(90, cell());
678        let mut surface = plain_surface(64, 64);
679        let mut wire = Vec::new();
680        presenter
681            .render_frame(&mut wire, &surface, crate::surface::test_support::area())
682            .expect("writing to a Vec cannot fail");
683        assert!(
684            !String::from_utf8_lossy(&wire).contains("d=R"),
685            "no tiles exist yet"
686        );
687        surface.set(
688            katui_scene::surface::PixelPos::new(40, 40),
689            Rgba::solid(255, 255, 255),
690        );
691        presenter
692            .render_frame(&mut wire, &surface, crate::surface::test_support::area())
693            .expect("writing to a Vec cannot fail");
694        wire.clear();
695        for y in (0..64).step_by(8) {
696            for x in (0..64).step_by(8) {
697                surface.set(
698                    katui_scene::surface::PixelPos::new(x, y),
699                    Rgba::solid(1, 2, 3),
700                );
701            }
702        }
703        presenter
704            .render_frame(&mut wire, &surface, crate::surface::test_support::area())
705            .expect("writing to a Vec cannot fail");
706        let text = String::from_utf8_lossy(&wire);
707        let placed = text.find("a=p,i=").expect("the new base is placed");
708        let freed = text.find("d=I").expect("the stale tiles are freed");
709        assert!(
710            !text.contains("d=R"),
711            "Ghostty frees every image on a range delete"
712        );
713        assert!(placed < freed, "the new base appears before the tiles go");
714    }
715
716    fn space() -> katui_proto::kitty::IdSpace {
717        katui_proto::kitty::IdSpace::of_process(4242)
718    }
719
720    const VIRTUAL: &str = ",U=1";
721
722    #[test]
723    fn a_multiplexer_grid_shows_every_tile_through_placeholders_and_never_a_whole_image() {
724        let mut presenter = PatchedKitty::new(90, cell()).for_multiplexer(space(), false);
725        let surface = plain_surface(64, 64);
726        let mut wire = Vec::new();
727        presenter
728            .render_frame(&mut wire, &surface, crate::surface::test_support::area())
729            .expect("writing to a Vec cannot fail");
730        let text = String::from_utf8_lossy(&wire);
731        assert!(!text.contains("s=64"), "no whole image travels");
732        assert_eq!(
733            text.matches(VIRTUAL).count(),
734            64,
735            "every tile has a virtual placement"
736        );
737        assert_eq!(
738            text.matches('\u{10eeee}').count(),
739            16 * 16,
740            "placeholder text covers every cell once"
741        );
742        assert!(!text.contains(",P="), "no tile needs a parent placement");
743    }
744
745    #[test]
746    fn a_multiplexer_frame_uploads_every_tile_before_showing_any() {
747        let mut presenter = PatchedKitty::new(90, cell()).for_multiplexer(space(), false);
748        let mut wire = Vec::new();
749        presenter
750            .render_frame(
751                &mut wire,
752                &plain_surface(64, 64),
753                crate::surface::test_support::area(),
754            )
755            .expect("writing to a Vec cannot fail");
756        let text = String::from_utf8_lossy(&wire);
757        let last_upload = text.rfind("a=t,").expect("tiles travel");
758        let first_show = text.find(VIRTUAL).expect("tiles show");
759        assert!(last_upload < first_show, "the frame appears at once");
760    }
761
762    #[test]
763    fn a_repainted_grid_resends_every_tile() {
764        let mut presenter = PatchedKitty::new(90, cell()).for_multiplexer(space(), false);
765        let surface = plain_surface(64, 64);
766        let mut wire = Vec::new();
767        presenter
768            .render_frame(&mut wire, &surface, crate::surface::test_support::area())
769            .expect("writing to a Vec cannot fail");
770        presenter.repaint();
771        wire.clear();
772        presenter
773            .render_frame(&mut wire, &surface, crate::surface::test_support::area())
774            .expect("writing to a Vec cannot fail");
775        let text = String::from_utf8_lossy(&wire);
776        assert_eq!(text.matches(VIRTUAL).count(), 64, "every tile returns");
777        assert_eq!(text.matches("a=t,").count(), 64, "only the tiles travel");
778        assert!(!text.contains("d=R"), "old tiles stay until replaced");
779        wire.clear();
780        presenter
781            .render_frame(&mut wire, &surface, crate::surface::test_support::area())
782            .expect("writing to a Vec cannot fail");
783        assert!(wire.is_empty(), "one repaint request repaints once");
784    }
785
786    #[test]
787    fn a_multiplexer_grid_frees_the_old_tiles_on_a_resize() {
788        let mut presenter = PatchedKitty::new(90, cell()).for_multiplexer(space(), false);
789        let mut wire = Vec::new();
790        presenter
791            .render_frame(
792                &mut wire,
793                &plain_surface(64, 64),
794                crate::surface::test_support::area(),
795            )
796            .expect("writing to a Vec cannot fail");
797        wire.clear();
798        presenter
799            .render_frame(
800                &mut wire,
801                &plain_surface(32, 32),
802                crate::surface::test_support::area(),
803            )
804            .expect("writing to a Vec cannot fail");
805        let text = String::from_utf8_lossy(&wire);
806        let freed = text.find("d=I").expect("the old grid is freed");
807        let placed = text.find("a=p,").expect("the new grid is placed");
808        assert!(freed < placed);
809    }
810
811    #[test]
812    fn a_base_retransmit_never_reuses_the_visible_id() {
813        let mut presenter = PatchedKitty::with_tiles(90, cell(), 8, 0);
814        let mut surface = plain_surface(16, 16);
815        let mut first = Vec::new();
816        presenter
817            .render_frame(&mut first, &surface, crate::surface::test_support::area())
818            .expect("writing to a Vec cannot fail");
819        surface.set(
820            katui_scene::surface::PixelPos::new(1, 1),
821            Rgba::solid(9, 9, 9),
822        );
823        let mut second = Vec::new();
824        presenter
825            .render_frame(&mut second, &surface, crate::surface::test_support::area())
826            .expect("writing to a Vec cannot fail");
827        let text = String::from_utf8_lossy(&second);
828        let placed = text.find("a=p,i=91").expect("the alternate id is placed");
829        let freed = text.find("d=I,i=90").expect("the old base is freed");
830        assert!(placed < freed, "something is always on screen");
831    }
832
833    #[test]
834    fn a_tile_alternates_ids_and_frees_the_previous_one_after_placing() {
835        let mut presenter = PatchedKitty::new(90, cell());
836        let mut surface = plain_surface(64, 64);
837        let mut wire = Vec::new();
838        presenter
839            .render_frame(&mut wire, &surface, crate::surface::test_support::area())
840            .expect("writing to a Vec cannot fail");
841        for shade in [10, 20] {
842            surface.set(
843                katui_scene::surface::PixelPos::new(40, 40),
844                Rgba::solid(shade, shade, shade),
845            );
846            wire.clear();
847            presenter
848                .render_frame(&mut wire, &surface, crate::surface::test_support::area())
849                .expect("writing to a Vec cannot fail");
850        }
851        let text = String::from_utf8_lossy(&wire);
852        let placed = text.find("a=p,i=82033").expect("the second generation");
853        let freed = text.find("d=I,i=82032").expect("the first is freed");
854        assert!(placed < freed, "the old tile stays until the new one shows");
855    }
856
857    #[test]
858    fn a_whole_surface_change_falls_back_to_one_retransmit() {
859        // 128x128 at 8px tiles is 256 tiles: everything dirty blows
860        // the patch budget
861        let mut presenter = PatchedKitty::new(90, cell());
862        let mut surface = plain_surface(128, 128);
863        let big_area = CellRect {
864            x: 0,
865            y: 0,
866            columns: 32,
867            rows: 32,
868        };
869        let mut warmup = Vec::new();
870        presenter
871            .render_frame(&mut warmup, &surface, big_area)
872            .expect("writing to a Vec cannot fail");
873        for y in 0..128u32 {
874            for x in 0..128u32 {
875                surface.set(
876                    katui_scene::surface::PixelPos::new(x, y),
877                    Rgba::solid(200, 40, 40),
878                );
879            }
880        }
881        let mut out = Vec::new();
882        presenter
883            .render_frame(&mut out, &surface, big_area)
884            .expect("writing to a Vec cannot fail");
885        let text = String::from_utf8_lossy(&out);
886        assert!(
887            text.contains("a=t"),
888            "everything dirty means one full frame"
889        );
890    }
891
892    #[test]
893    fn a_size_change_falls_back_to_one_retransmit() {
894        let mut presenter = PatchedKitty::new(90, cell());
895        let mut out = Vec::new();
896        presenter
897            .render_frame(
898                &mut out,
899                &plain_surface(64, 64),
900                crate::surface::test_support::area(),
901            )
902            .expect("writing to a Vec cannot fail");
903        let mut resized = Vec::new();
904        presenter
905            .render_frame(
906                &mut resized,
907                &plain_surface(32, 32),
908                crate::surface::test_support::area(),
909            )
910            .expect("writing to a Vec cannot fail");
911        let text = String::from_utf8_lossy(&resized);
912        assert!(text.contains("a=t"), "a resize retransmits whole");
913    }
914}