1mod animated;
7mod blocks;
8mod buffer;
9mod full;
10mod patched;
11mod screen;
12mod sixel;
13
14pub use animated::AnimatedKitty;
15pub use blocks::BlocksSurface;
16pub use buffer::BufferSurface;
17pub use full::KittySurface;
18pub use patched::PatchedKitty;
19pub use screen::PixelScreen;
20pub use sixel::SixelSurface;
21
22use crate::Sink;
23use std::io;
24
25use katui_proto::kitty::{
26 Format, ImageId as KittyImage, Medium, Place, PlacementId as KittyPlacement, Transmit,
27};
28use katui_scene::cell::Position;
29use katui_scene::placement::CellRect;
30use katui_scene::surface::Surface;
31
32use crate::cells::move_cursor;
33
34pub(crate) fn damage_is_dense(surface: &Surface, tile_side: u32, damaged: usize) -> bool {
37 const FINE_GRID_TILES: u64 = 8;
40 let across = surface.width().div_ceil(tile_side);
41 let down = surface.height().div_ceil(tile_side);
42 let total = u64::from(across.saturating_mul(down).max(1));
43 total >= FINE_GRID_TILES && 4 * u64::try_from(damaged).unwrap_or(u64::MAX) >= total
44}
45
46pub trait Stash: std::fmt::Debug + Send {
49 fn put(&mut self, pixels: &[u8]) -> io::Result<Medium>;
51}
52
53pub(crate) fn transmit_pixels<W: Sink>(
56 bytes: &mut W,
57 image: KittyImage,
58 pixels: &[u8],
59 (width, height): (u32, u32),
60 stash: Option<&mut (dyn Stash + 'static)>,
61) -> io::Result<()> {
62 let mut transmit = Transmit::new(image, Format::Rgba, width, height, pixels);
63 transmit.compressed = true;
64 if let Some(stash) = stash {
65 transmit.medium = stash.put(pixels)?;
66 }
67 for frame in transmit.encode()? {
68 bytes.write_all(&frame)?;
69 }
70 Ok(())
71}
72
73pub(crate) fn write_whole_image<W: Sink>(
77 bytes: &mut W,
78 image: KittyImage,
79 surface: &Surface,
80 area: CellRect,
81 stash: Option<&mut (dyn Stash + 'static)>,
82) -> io::Result<()> {
83 let size = (surface.width(), surface.height());
84 transmit_pixels(bytes, image, surface.pixels(), size, stash)?;
85 move_cursor(
86 bytes,
87 Position {
88 x: area.x,
89 y: area.y,
90 },
91 )?;
92 let mut place = Place::new(image);
93 place.placement = KittyPlacement(1);
94 place.columns = u32::from(area.columns);
95 place.rows = u32::from(area.rows);
96 bytes.write_all(&place.encode())
97}
98
99#[cfg(test)]
101pub(crate) mod test_support {
102 use katui_scene::placement::CellRect;
103 use katui_scene::surface::{Rgba, Surface};
104
105 pub(crate) fn solid(width: u32, height: u32, color: Rgba) -> Surface {
106 let mut surface = Surface::new(width, height);
107 for y in 0..height {
108 for x in 0..width {
109 surface.set(katui_scene::surface::PixelPos::new(x, y), color);
110 }
111 }
112 surface
113 }
114
115 pub(crate) fn area() -> CellRect {
117 CellRect::full(16, 16)
118 }
119}
120
121#[cfg(test)]
125mod budget_tests {
126 use katui_proto::probe::CellPixels;
127 use katui_scene::placement::CellRect;
128 use katui_scene::surface::{PixelPos, Rgba, Surface};
129
130 use super::{AnimatedKitty, BlocksSurface, PatchedKitty, SixelSurface};
131
132 const WIDTH: u32 = 640;
133 const HEIGHT: u32 = 360;
134 const SPRITE: u32 = 16;
135 const FRAMES: u32 = 30;
136 const BUDGET: usize = 8 * 1024;
138
139 fn background() -> Surface {
140 let mut surface = Surface::new(WIDTH, HEIGHT);
141 for y in 0..HEIGHT {
142 for x in 0..WIDTH {
143 let shade = Rgba::solid((x / 3) as u8, (y / 2) as u8, 90);
144 surface.set(PixelPos::new(x, y), shade);
145 }
146 }
147 surface
148 }
149
150 fn scene(background: &Surface, frame: u32) -> Surface {
151 let mut surface = background.clone();
152 let left = 40 + frame * 4;
153 for y in 100..100 + SPRITE {
154 for x in left..left + SPRITE {
155 surface.set(PixelPos::new(x, y), Rgba::solid(255, 240, 40));
156 }
157 }
158 surface
159 }
160
161 fn worst_frame(mut render: impl FnMut(&mut Vec<u8>, &Surface) -> usize) -> usize {
163 let background = background();
164 let mut out = Vec::new();
165 render(&mut out, &scene(&background, 0));
166 (1..FRAMES)
167 .map(|frame| {
168 out.clear();
169 render(&mut out, &scene(&background, frame))
170 })
171 .max()
172 .unwrap_or(0)
173 }
174
175 fn area() -> CellRect {
176 CellRect::full(80, 20)
177 }
178
179 #[test]
180 fn a_moving_sprite_costs_a_few_kilobytes_a_frame_in_every_strategy() {
181 let cell = CellPixels {
182 width: 8,
183 height: 18,
184 };
185 let mut patched = PatchedKitty::with_tiles(900, cell, 32, 64);
186 let patched_worst = worst_frame(|out, surface| {
187 patched
188 .render_frame(out, surface, area())
189 .expect("writing to a Vec cannot fail")
190 });
191 let mut tiled = PatchedKitty::with_tiles(900, cell, 32, 64)
192 .for_multiplexer(katui_proto::kitty::IdSpace::of_process(4242), false);
193 let tiled_worst = worst_frame(|out, surface| {
194 tiled
195 .render_frame(out, surface, area())
196 .expect("writing to a Vec cannot fail")
197 });
198 let mut animated = AnimatedKitty::new(900, 60, 32, 64);
199 let animated_worst = worst_frame(|out, surface| {
200 animated
201 .render_frame(out, surface, area())
202 .expect("writing to a Vec cannot fail")
203 });
204 let mut sixel = SixelSurface::new(cell);
205 let sixel_worst = worst_frame(|out, surface| {
206 sixel
207 .render_frame(out, surface, 80, 21)
208 .expect("writing to a Vec cannot fail")
209 });
210 let mut blocks = BlocksSurface::new();
211 let blocks_worst = worst_frame(|out, surface| {
212 blocks
213 .render_frame(out, surface)
214 .expect("writing to a Vec cannot fail")
215 });
216 let worst = [
217 ("patched", patched_worst),
218 ("multiplexer tiles", tiled_worst),
219 ("animated", animated_worst),
220 ("sixel", sixel_worst),
221 ("blocks", blocks_worst),
222 ];
223 for (strategy, bytes) in worst {
224 assert!(bytes > 0, "{strategy} sends the moved sprite");
225 assert!(
226 bytes <= BUDGET,
227 "{strategy} spent {bytes} bytes on one frame"
228 );
229 }
230 }
231}