aboutsummaryrefslogtreecommitdiff
path: root/src/kitty_graphics.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/kitty_graphics.rs')
-rw-r--r--src/kitty_graphics.rs269
1 files changed, 262 insertions, 7 deletions
diff --git a/src/kitty_graphics.rs b/src/kitty_graphics.rs
index 4564bde6..2e83841c 100644
--- a/src/kitty_graphics.rs
+++ b/src/kitty_graphics.rs
@@ -120,6 +120,8 @@ struct ClippedPlacement {
pub(crate) struct HostGraphicsCache {
images: HashMap<u32, ImageSignature>,
placements: HashMap<(u32, u32), PlacementSignature>,
+ /// Host image currently backing each (pane, source image id) pair.
+ sources: HashMap<(PaneId, u32), u32>,
view: Option<HostViewKey>,
}
@@ -204,6 +206,7 @@ pub(crate) fn encode_local_pane_graphics(
view_changed,
&mut cache.images,
&mut cache.placements,
+ &mut cache.sources,
);
tracing::debug!(
placements = placements.len(),
@@ -267,7 +270,16 @@ fn encode_graphics_update(
view_changed: bool,
host_images: &mut HashMap<u32, ImageSignature>,
host_placements: &mut HashMap<(u32, u32), PlacementSignature>,
+ sources: &mut HashMap<(PaneId, u32), u32>,
) {
+ // Prune sources that are no longer visible: a stale entry would keep its
+ // old host image referenced and block the superseded-image delete.
+ let current_sources: HashSet<(PaneId, u32)> = placements
+ .iter()
+ .map(|placement| (placement.pane_id, placement.placement.image_id))
+ .collect();
+ sources.retain(|source, _| current_sources.contains(source));
+
let mut current_placements = HashSet::new();
for placement in placements {
let clipped = clipped_placement(placement);
@@ -318,6 +330,16 @@ fn encode_graphics_update(
}
}
+ release_superseded_source_image(
+ bytes,
+ sources,
+ host_images,
+ host_placements,
+ &mut current_placements,
+ (placement.pane_id, placement.placement.image_id),
+ host_id,
+ );
+
// A different view can repaint the same cells with text or overlays and
// leave the host-side Kitty placement state out of sync with this cache.
// Re-emit the placement even when its geometry signature is unchanged.
@@ -359,6 +381,36 @@ fn encode_graphics_update(
}
}
+/// Records that `source` is now backed by `host_id` and deletes the host
+/// image it previously pointed at once no other source references it.
+fn release_superseded_source_image(
+ bytes: &mut Vec<u8>,
+ sources: &mut HashMap<(PaneId, u32), u32>,
+ host_images: &mut HashMap<u32, ImageSignature>,
+ host_placements: &mut HashMap<(u32, u32), PlacementSignature>,
+ current_placements: &mut HashSet<(u32, u32)>,
+ source: (PaneId, u32),
+ host_id: u32,
+) {
+ let Some(previous) = sources.insert(source, host_id) else {
+ return;
+ };
+ if previous == host_id || sources.values().any(|id| *id == previous) {
+ return;
+ }
+ encode_delete_image(bytes, previous);
+ host_images.remove(&previous);
+ // The `d=I` delete also removes the image's placements host-side.
+ host_placements.retain(|(image_id, placement_id), _| {
+ if *image_id == previous {
+ current_placements.remove(&(*image_id, *placement_id));
+ false
+ } else {
+ true
+ }
+ });
+}
+
pub(crate) fn clear_all_host_graphics() -> io::Result<()> {
let cache = LOCAL_HOST_GRAPHICS.get_or_init(|| Mutex::new(HostGraphicsCache::default()));
let mut bytes = Vec::new();
@@ -399,6 +451,7 @@ impl HostGraphicsCache {
}
self.images.clear();
self.placements.clear();
+ self.sources.clear();
self.view = None;
bytes
}
@@ -865,6 +918,7 @@ mod tests {
fn graphics_update_uploads_once_then_repositions_only() {
let mut images = HashMap::new();
let mut placements = HashMap::new();
+ let mut sources = HashMap::new();
let mut bytes = Vec::new();
let placement = test_placement(0, 0);
@@ -874,6 +928,7 @@ mod tests {
false,
&mut images,
&mut placements,
+ &mut sources,
);
let first = String::from_utf8_lossy(&bytes);
assert!(first.contains("a=t"));
@@ -881,7 +936,14 @@ mod tests {
bytes.clear();
let same = test_placement(0, 0);
- encode_graphics_update(&mut bytes, &[same], false, &mut images, &mut placements);
+ encode_graphics_update(
+ &mut bytes,
+ &[same],
+ false,
+ &mut images,
+ &mut placements,
+ &mut sources,
+ );
assert!(bytes.is_empty());
let mut z_changed = test_placement(0, 0);
@@ -892,6 +954,7 @@ mod tests {
false,
&mut images,
&mut placements,
+ &mut sources,
);
let z_changed_bytes = String::from_utf8_lossy(&bytes);
assert!(!z_changed_bytes.contains("a=t"));
@@ -899,7 +962,14 @@ mod tests {
bytes.clear();
let moved = test_placement(0, 1);
- encode_graphics_update(&mut bytes, &[moved], false, &mut images, &mut placements);
+ encode_graphics_update(
+ &mut bytes,
+ &[moved],
+ false,
+ &mut images,
+ &mut placements,
+ &mut sources,
+ );
let moved_bytes = String::from_utf8_lossy(&bytes);
assert!(!moved_bytes.contains("a=t"));
assert!(moved_bytes.contains("a=p"));
@@ -909,6 +979,7 @@ mod tests {
fn view_change_redisplays_unchanged_visible_placement() {
let mut images = HashMap::new();
let mut placements = HashMap::new();
+ let mut sources = HashMap::new();
let mut bytes = Vec::new();
let placement = test_placement(0, 0);
@@ -918,12 +989,20 @@ mod tests {
false,
&mut images,
&mut placements,
+ &mut sources,
);
assert_eq!(placements.len(), 1);
bytes.clear();
let same = test_placement(0, 0);
- encode_graphics_update(&mut bytes, &[same], true, &mut images, &mut placements);
+ encode_graphics_update(
+ &mut bytes,
+ &[same],
+ true,
+ &mut images,
+ &mut placements,
+ &mut sources,
+ );
let redisplay = String::from_utf8_lossy(&bytes);
assert!(!redisplay.contains("a=t"));
assert!(redisplay.contains("a=p"));
@@ -942,6 +1021,7 @@ mod tests {
false,
&mut cache.images,
&mut cache.placements,
+ &mut cache.sources,
);
assert_eq!(cache.images.len(), 1);
assert_eq!(cache.placements.len(), 1);
@@ -954,6 +1034,7 @@ mod tests {
false,
&mut cache.images,
&mut cache.placements,
+ &mut cache.sources,
);
let redisplay = String::from_utf8_lossy(&bytes);
@@ -968,6 +1049,7 @@ mod tests {
fn scrollback_offset_change_redisplays_placement() {
let mut images = HashMap::new();
let mut placements = HashMap::new();
+ let mut sources = HashMap::new();
let mut bytes = Vec::new();
let placement = test_placement(0, 0);
@@ -977,12 +1059,20 @@ mod tests {
false,
&mut images,
&mut placements,
+ &mut sources,
);
bytes.clear();
let mut scrolled = test_placement(0, 0);
scrolled.scrollback_offset = 3;
- encode_graphics_update(&mut bytes, &[scrolled], false, &mut images, &mut placements);
+ encode_graphics_update(
+ &mut bytes,
+ &[scrolled],
+ false,
+ &mut images,
+ &mut placements,
+ &mut sources,
+ );
let redisplay = String::from_utf8_lossy(&bytes);
assert!(!redisplay.contains("a=t"));
assert!(redisplay.contains("a=p"));
@@ -992,6 +1082,7 @@ mod tests {
fn empty_image_data_does_not_mark_image_uploaded() {
let mut images = HashMap::new();
let mut placements = HashMap::new();
+ let mut sources = HashMap::new();
let mut bytes = Vec::new();
let mut placement = test_placement(0, 0);
placement.placement.data.clear();
@@ -1002,6 +1093,7 @@ mod tests {
false,
&mut images,
&mut placements,
+ &mut sources,
);
assert!(bytes.is_empty());
@@ -1013,10 +1105,18 @@ mod tests {
fn same_image_signature_reuses_host_upload_across_source_image_ids() {
let mut images = HashMap::new();
let mut placements = HashMap::new();
+ let mut sources = HashMap::new();
let mut bytes = Vec::new();
let first = test_placement(0, 0);
- encode_graphics_update(&mut bytes, &[first], false, &mut images, &mut placements);
+ encode_graphics_update(
+ &mut bytes,
+ &[first],
+ false,
+ &mut images,
+ &mut placements,
+ &mut sources,
+ );
assert_eq!(images.len(), 1);
assert_eq!(placements.len(), 1);
@@ -1031,6 +1131,7 @@ mod tests {
false,
&mut images,
&mut placements,
+ &mut sources,
);
let reused = String::from_utf8_lossy(&bytes);
@@ -1041,9 +1142,146 @@ mod tests {
}
#[test]
+ fn replaced_image_content_deletes_superseded_host_image() {
+ let mut images = HashMap::new();
+ let mut placements = HashMap::new();
+ let mut sources = HashMap::new();
+ let mut bytes = Vec::new();
+ let first = test_placement(0, 0);
+
+ encode_graphics_update(
+ &mut bytes,
+ &[first],
+ false,
+ &mut images,
+ &mut placements,
+ &mut sources,
+ );
+ assert_eq!(images.len(), 1);
+ let superseded_host_id = *images.keys().next().expect("uploaded host image");
+
+ // Same source image id, new pixel content: the fresh content maps to
+ // a fresh host image id, so the replaced one must be deleted.
+ bytes.clear();
+ let mut changed = test_placement(0, 0);
+ changed.placement.data_fingerprint = 43;
+ encode_graphics_update(
+ &mut bytes,
+ &[changed],
+ false,
+ &mut images,
+ &mut placements,
+ &mut sources,
+ );
+
+ let update = String::from_utf8_lossy(&bytes);
+ assert!(update.contains("a=t"), "changed content re-uploads");
+ assert!(
+ update.contains(&format!("a=d,d=I,i={superseded_host_id}")),
+ "superseded host image is deleted"
+ );
+ assert_eq!(images.len(), 1);
+ assert_eq!(placements.len(), 1);
+ }
+
+ #[test]
+ fn shared_host_image_survives_while_another_source_references_it() {
+ fn twin_placement() -> HostPlacement {
+ let mut twin = test_placement(5, 5);
+ twin.placement.image_id = 8;
+ twin.placement.placement_id = 4;
+ twin
+ }
+
+ let mut images = HashMap::new();
+ let mut placements = HashMap::new();
+ let mut sources = HashMap::new();
+ let mut bytes = Vec::new();
+
+ encode_graphics_update(
+ &mut bytes,
+ &[test_placement(0, 0), twin_placement()],
+ false,
+ &mut images,
+ &mut placements,
+ &mut sources,
+ );
+ assert_eq!(images.len(), 1, "same content dedups to one host image");
+
+ // One source moves to new content while the other still shows the
+ // old image: the shared host image must survive.
+ bytes.clear();
+ let mut changed = test_placement(0, 0);
+ changed.placement.data_fingerprint = 43;
+ encode_graphics_update(
+ &mut bytes,
+ &[changed, twin_placement()],
+ false,
+ &mut images,
+ &mut placements,
+ &mut sources,
+ );
+
+ let update = String::from_utf8_lossy(&bytes);
+ assert!(!update.contains("a=d,d=I"), "shared host image survives");
+ assert_eq!(images.len(), 2);
+ }
+
+ #[test]
+ fn stale_source_entry_does_not_block_superseded_image_delete() {
+ fn twin_placement() -> HostPlacement {
+ let mut twin = test_placement(5, 5);
+ twin.placement.image_id = 8;
+ twin.placement.placement_id = 4;
+ twin
+ }
+
+ let mut images = HashMap::new();
+ let mut placements = HashMap::new();
+ let mut sources = HashMap::new();
+ let mut bytes = Vec::new();
+
+ encode_graphics_update(
+ &mut bytes,
+ &[test_placement(0, 0), twin_placement()],
+ false,
+ &mut images,
+ &mut placements,
+ &mut sources,
+ );
+ assert_eq!(images.len(), 1);
+ assert_eq!(sources.len(), 2);
+ let shared_host_id = *images.keys().next().expect("uploaded host image");
+
+ // The twin source is gone and the survivor changed content: the
+ // vanished source's stale entry must not keep the old host image
+ // alive.
+ bytes.clear();
+ let mut changed = test_placement(0, 0);
+ changed.placement.data_fingerprint = 43;
+ encode_graphics_update(
+ &mut bytes,
+ &[changed],
+ false,
+ &mut images,
+ &mut placements,
+ &mut sources,
+ );
+
+ let update = String::from_utf8_lossy(&bytes);
+ assert!(
+ update.contains(&format!("a=d,d=I,i={shared_host_id}")),
+ "old host image is deleted once its last live source moves on"
+ );
+ assert_eq!(images.len(), 1);
+ assert_eq!(sources.len(), 1);
+ }
+
+ #[test]
fn stale_placement_deletes_placement_not_image_immediately() {
let mut images = HashMap::new();
let mut placements = HashMap::new();
+ let mut sources = HashMap::new();
let mut bytes = Vec::new();
let placement = test_placement(0, 0);
@@ -1053,11 +1291,19 @@ mod tests {
false,
&mut images,
&mut placements,
+ &mut sources,
);
assert_eq!(placements.len(), 1);
bytes.clear();
- encode_graphics_update(&mut bytes, &[], false, &mut images, &mut placements);
+ encode_graphics_update(
+ &mut bytes,
+ &[],
+ false,
+ &mut images,
+ &mut placements,
+ &mut sources,
+ );
let delete = String::from_utf8_lossy(&bytes);
assert!(delete.contains("a=d,d=i"));
assert!(!delete.contains("d=I"));
@@ -1069,6 +1315,7 @@ mod tests {
fn view_change_deletes_stale_placement_immediately() {
let mut images = HashMap::new();
let mut placements = HashMap::new();
+ let mut sources = HashMap::new();
let mut bytes = Vec::new();
let placement = test_placement(0, 0);
@@ -1078,9 +1325,17 @@ mod tests {
false,
&mut images,
&mut placements,
+ &mut sources,
);
bytes.clear();
- encode_graphics_update(&mut bytes, &[], true, &mut images, &mut placements);
+ encode_graphics_update(
+ &mut bytes,
+ &[],
+ true,
+ &mut images,
+ &mut placements,
+ &mut sources,
+ );
let delete = String::from_utf8_lossy(&bytes);
assert!(delete.contains("a=d,d=i"));