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