diff options
Diffstat (limited to 'src/kitty_graphics.rs')
| -rw-r--r-- | src/kitty_graphics.rs | 269 |
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")); |
