diff options
Diffstat (limited to 'src/kitty_graphics.rs')
| -rw-r--r-- | src/kitty_graphics.rs | 688 |
1 files changed, 132 insertions, 556 deletions
diff --git a/src/kitty_graphics.rs b/src/kitty_graphics.rs index 2eca21f5..6bca9443 100644 --- a/src/kitty_graphics.rs +++ b/src/kitty_graphics.rs @@ -9,21 +9,20 @@ use base64::Engine; use ratatui::layout::Rect; use crate::app::state::AppState; -use crate::ghostty::{ - KittyImageDescriptor, KittyImageFormat, KittyImagePlacement, KittyPlacementRenderInfo, -}; -use crate::layout::{PaneId, PaneInfo}; +use crate::ghostty::{KittyImageDescriptor, KittyImageFormat, KittyImagePlacement}; +use crate::layout::PaneId; use crate::terminal::TerminalRuntimeRegistry; +pub(crate) mod output; pub(crate) mod surface; +pub(crate) use output::{GraphicsOperation, GraphicsOutput}; +use std::sync::Arc; + const KITTY_CHUNK_BYTES: usize = 3072; -const MAX_OVERSIZED_SOURCES: usize = 256; pub(crate) const HEADLESS_GRAPHICS_TRANSACTION_BUDGET: usize = crate::protocol::MAX_GRAPHICS_FRAME_SIZE - crate::protocol::MAX_FRAME_SIZE; const HOST_IMAGE_ID_BASE: u32 = 10_000; -#[cfg(test)] -const PANE_GRAPHICS_IMAGE_ID_BIT: u32 = 1 << 31; #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub(crate) struct HostCellSize { @@ -39,6 +38,8 @@ impl HostCellSize { #[derive(Debug)] struct HostPlacement { + /// Client-local shared pixels; headless placements retain their inline data. + raw_data: Option<Arc<[u8]>>, pane_id: PaneId, host_image_id: Option<u32>, area: Rect, @@ -54,10 +55,6 @@ enum HostSourceKey { pane_id: PaneId, image_id: u32, }, - PaneLayer { - pane_id: PaneId, - layer_id: String, - }, ClientSurface { scope: String, source: crate::protocol::SurfaceGraphicsSource, @@ -109,7 +106,6 @@ pub(crate) struct HostGraphicsCache { placements: HashMap<(u32, u32), PlacementSignature>, /// Host image currently backing each (pane, source image id) pair. sources: HashMap<HostSourceKey, u32>, - oversized: HashMap<HostSourceKey, ImageSignature>, continuation: Option<(HostSourceKey, u32, usize)>, replay_placements: bool, replayed_placements: HashSet<(u32, u32)>, @@ -125,32 +121,12 @@ pub(crate) fn is_enabled() -> bool { KITTY_GRAPHICS_ENABLED.load(Ordering::Acquire) } +#[cfg(test)] pub(crate) struct EncodedGraphics { pub(crate) bytes: Vec<u8>, pub(crate) incomplete: bool, } -/// Whether appending `additional` bytes to the `current_len` bytes already -/// assembled keeps the transaction inside the caller's budget. Without a -/// budget the incremental path intentionally stays one transaction per call. -fn coalesced_transaction_fits( - current_len: usize, - additional: usize, - transaction_budget: Option<usize>, -) -> bool { - let Some(budget) = transaction_budget else { - return false; - }; - current_len.saturating_add(additional) <= budget -} - -fn image_transaction_fits(placement: &HostPlacement, budget: Option<usize>) -> bool { - let Some(budget) = budget else { - return true; - }; - image_transfer_estimated_size(placement.placement.data_len) <= budget -} - pub(crate) fn image_transfer_estimated_size(data_len: usize) -> usize { let encoded = data_len.div_ceil(3).saturating_mul(4); let command_overhead = data_len.div_ceil(KITTY_CHUNK_BYTES).saturating_mul(16) + 1024; @@ -164,23 +140,10 @@ fn placement_identity(placement: &HostPlacement) -> (HostSourceKey, u32) { ) } -fn source_order(source: &HostSourceKey) -> (u32, String) { - match source { - HostSourceKey::Terminal { pane_id, .. } => (pane_id.raw(), String::new()), - HostSourceKey::PaneLayer { pane_id, layer_id } => (pane_id.raw(), layer_id.clone()), - HostSourceKey::ClientSurface { scope, source } => { - let mut hasher = DefaultHasher::new(); - scope.hash(&mut hasher); - source.hash(&mut hasher); - (hasher.finish() as u32, format!("{source:?}")) - } - } -} - fn encode_placement_update( cache: &mut HostGraphicsCache, placement: &HostPlacement, -) -> Option<Vec<u8>> { +) -> Option<GraphicsOutput> { let (clipped, format_code) = clipped_placement(placement)?; let host_id = placement .host_image_id @@ -201,37 +164,30 @@ fn encode_placement_update( } let mut bytes = Vec::new(); - let mut displayed = false; + let mut output = GraphicsOutput::default(); if !image_current { - if cache.images.contains_key(&host_id) - && matches!(placement.source_key, HostSourceKey::PaneLayer { .. }) - { - if !encode_transmit_and_display( - &mut bytes, - placement, - clipped, - format_code, - host_id, - placement_id, - ) { - return None; - } - displayed = true; - } else { - if cache.images.contains_key(&host_id) { - encode_delete_image(&mut bytes, host_id); - cache.placements.retain(|(id, _), _| *id != host_id); - cache.replayed_placements.retain(|(id, _)| *id != host_id); - } - if !encode_upload_image(&mut bytes, placement, format_code, host_id) { + if cache.images.contains_key(&host_id) { + encode_delete_image(&mut bytes, host_id); + cache.placements.retain(|(id, _), _| *id != host_id); + cache.replayed_placements.retain(|(id, _)| *id != host_id); + } + if let Some(data) = &placement.raw_data { + if data.is_empty() { return None; } + output.push_bytes(std::mem::take(&mut bytes)); + output.operations.push(GraphicsOperation::Upload { + control: upload_control(placement, format_code, host_id), + data: Arc::clone(data), + }); + } else if !encode_upload_image(&mut bytes, placement, format_code, host_id) { + return None; } cache.images.insert(host_id, image_signature); } release_superseded_source_image(&mut bytes, cache, placement.source_key.clone(), host_id); - if !displayed && !placement_current { + if !placement_current { encode_display_placement( &mut bytes, clipped, @@ -244,7 +200,8 @@ fn encode_placement_update( if cache.replay_placements { cache.replayed_placements.insert(key); } - Some(bytes) + output.push_bytes(bytes); + Some(output) } fn release_superseded_source_image( @@ -265,13 +222,22 @@ fn release_superseded_source_image( cache.replayed_placements.retain(|(id, _)| *id != previous); } +#[cfg(test)] fn encode_graphics_update_incremental( cache: &mut HostGraphicsCache, placements: &[HostPlacement], - live_pane_sources: &HashSet<HostSourceKey>, - transaction_budget: Option<usize>, - coalesce_placements: bool, ) -> EncodedGraphics { + let (output, incomplete) = encode_graphics_output_incremental(cache, placements); + EncodedGraphics { + bytes: output.into_inline_bytes(), + incomplete, + } +} + +fn encode_graphics_output_incremental( + cache: &mut HostGraphicsCache, + placements: &[HostPlacement], +) -> (GraphicsOutput, bool) { let desired_sources = placements .iter() .map(|placement| placement.source_key.clone()) @@ -301,48 +267,12 @@ fn encode_graphics_update_incremental( .map(|index| index + 1) .or_else(|| cache.continuation.as_ref().map(|cursor| cursor.2)) .map_or(0, |index| index % placements.len().max(1)); - let mut bytes = Vec::new(); + let mut bytes = GraphicsOutput::default(); let mut emitted = false; - let mut dead_sources = cache + cache .sources - .keys() - .filter(|source| { - matches!(source, HostSourceKey::PaneLayer { .. }) - && !live_pane_sources.contains(*source) - }) - .cloned() - .collect::<Vec<_>>(); - dead_sources.sort_by_key(source_order); - for source in dead_sources { - let host_id = cache.sources[&source]; - let last_reference = !cache - .sources - .iter() - .any(|(other, id)| *other != source && *id == host_id); - if emitted && last_reference { - return EncodedGraphics { - bytes, - incomplete: true, - }; - } - cache.sources.remove(&source); - if last_reference { - encode_delete_image(&mut bytes, host_id); - cache.images.remove(&host_id); - cache.placements.retain(|(id, _), _| *id != host_id); - cache.replayed_placements.retain(|(id, _)| *id != host_id); - emitted = true; - } - } - cache.sources.retain(|source, _| { - matches!(source, HostSourceKey::PaneLayer { .. }) || desired_sources.contains(source) - }); - cache.oversized.retain(|source, _| { - matches!(source, HostSourceKey::Terminal { .. }) - || live_pane_sources.contains(source) - || desired_sources.contains(source) - }); + .retain(|source, _| desired_sources.contains(source)); let mut stale = cache .placements @@ -351,56 +281,22 @@ fn encode_graphics_update_incremental( .copied() .collect::<Vec<_>>(); stale.sort_unstable(); - let mut stale_image = None; for key @ (host_id, placement_id) in stale { let mut transaction = Vec::new(); encode_delete_placement(&mut transaction, host_id, placement_id); - let same_image = stale_image == Some(host_id); - if emitted - && !(coalesce_placements - && same_image - && coalesced_transaction_fits(bytes.len(), transaction.len(), transaction_budget)) - { - return EncodedGraphics { - bytes, - incomplete: true, - }; + if emitted { + return (bytes, true); } - bytes.extend(transaction); + bytes.push_bytes(transaction); cache.placements.remove(&key); cache.replayed_placements.remove(&key); emitted = true; - stale_image = Some(host_id); } - // Keep unrelated images isolated, but treat every row of one logical image - // as part of its upload or replacement transaction. Sending only the first - // row exposes the blank placeholder cells until later frames catch up. - let coalesce_pass = coalesce_placements && !emitted; - let mut coalesce_target = None; + // Preserve one cache mutation transaction per incremental call. for offset in 0..placements.len() { let index = (start + offset) % placements.len(); let placement = &placements[index]; - let signature = image_signature(placement, kitty_format_code(placement.placement.format)); - if transaction_budget.is_some() - && cache.oversized.get(&placement.source_key) == Some(&signature) - { - continue; - } - cache.oversized.remove(&placement.source_key); - let host_id = placement - .host_image_id - .unwrap_or_else(|| host_image_id(placement.pane_id, &placement.placement)); - let image_cached = cache.images.get(&host_id) == Some(&signature); - // With the image uploaded and the source already bound to it, the - // transaction is a re-display only: no upload and no superseded-image - // delete from `release_superseded_source_image`. - let pure_redisplay = - image_cached && cache.sources.get(&placement.source_key) == Some(&host_id); - if !image_cached && !image_transaction_fits(placement, transaction_budget) { - cache.quarantine_oversized(placement.source_key.clone(), signature); - continue; - } let mut candidate = cache.clone(); let Some(transaction) = encode_placement_update(&mut candidate, placement) else { continue; @@ -409,47 +305,26 @@ fn encode_graphics_update_incremental( *cache = candidate; continue; } - let same_logical_image = coalesce_target.as_ref().is_none_or(|(source, target_id)| { - source == &placement.source_key && *target_id == host_id - }); - if emitted - && !(coalesce_pass - && pure_redisplay - && same_logical_image - && coalesced_transaction_fits(bytes.len(), transaction.len(), transaction_budget)) - { - return EncodedGraphics { - bytes, - incomplete: true, - }; + if emitted { + return (bytes, true); } *cache = candidate; let (source, id) = placement_identity(placement); cache.continuation = Some((source, id, (index + 1) % placements.len())); bytes.extend(transaction); emitted = true; - if coalesce_pass && !pure_redisplay { - coalesce_target = Some((placement.source_key.clone(), host_id)); - } } cache.replay_placements = false; cache.replayed_placements.clear(); - EncodedGraphics { - bytes, - incomplete: false, - } + (bytes, false) } #[cfg(test)] -fn drain_graphics_updates( - cache: &mut HostGraphicsCache, - placements: &[HostPlacement], - live: &HashSet<HostSourceKey>, -) -> Vec<u8> { +fn drain_graphics_updates(cache: &mut HostGraphicsCache, placements: &[HostPlacement]) -> Vec<u8> { let mut bytes = Vec::new(); loop { - let encoded = encode_graphics_update_incremental(cache, placements, live, None, false); + let encoded = encode_graphics_update_incremental(cache, placements); bytes.extend(encoded.bytes); if !encoded.incomplete { return bytes; @@ -464,15 +339,6 @@ impl HostGraphicsCache { self.replayed_placements.clear(); } - fn quarantine_oversized(&mut self, source: HostSourceKey, signature: ImageSignature) { - if !self.oversized.contains_key(&source) && self.oversized.len() >= MAX_OVERSIZED_SOURCES { - if let Some(evicted) = self.oversized.keys().next().cloned() { - self.oversized.remove(&evicted); - } - } - self.oversized.insert(source, signature); - } - pub(crate) fn request_placement_replay(&mut self) { if !self.replay_placements { self.replay_placements = true; @@ -488,7 +354,6 @@ impl HostGraphicsCache { self.images.clear(); self.placements.clear(); self.sources.clear(); - self.oversized.clear(); self.reset_incremental_progress(); bytes } @@ -496,13 +361,10 @@ impl HostGraphicsCache { fn collect_visible_placements( app: &AppState, - graphics: &crate::app::pane_graphics::Runtime, terminal_runtimes: &TerminalRuntimeRegistry, surface: crate::ui::TabSurfaceView<'_>, cell_size: HostCellSize, - uploaded_images: &HashMap<u32, ImageSignature>, - oversized_images: &HashMap<HostSourceKey, ImageSignature>, - client_id: u64, + delivered_images: &HashMap<HostSourceKey, ImageSignature>, ) -> Vec<HostPlacement> { let Some(target) = surface.target else { tracing::debug!("collect_visible_placements: no tab surface target"); @@ -531,33 +393,6 @@ fn collect_visible_placements( ); let mut placements = Vec::new(); for info in surface.pane_infos { - let mut pane_layers = graphics - .slots - .iter() - .filter_map(|((pane_id, layer_id), slot)| { - (*pane_id == info.id) - .then(|| { - slot.layer.as_ref().and_then(|layer| { - (!layer.terminal_only() || slot.direct_client() == Some(client_id)) - .then_some((layer_id, slot.host_image_id, layer)) - }) - }) - .flatten() - }) - .collect::<Vec<_>>(); - pane_layers.sort_by_key(|(layer_id, _, layer)| (layer.z_index, layer_id.as_str())); - for (layer_id, host_image_id, layer) in pane_layers { - placements.push(pane_graphics_host_placement( - info, - layer_id, - host_image_id, - cell_size, - layer, - uploaded_images, - true, - )); - } - let runtime = match app.runtime_for_pane_in_workspace(terminal_runtimes, ws_idx, info.id) { Some(rt) => rt, None => { @@ -567,19 +402,17 @@ fn collect_visible_placements( }; let mut requested_images = HashSet::new(); for placement in runtime.kitty_image_placements_with_data_filter(|descriptor| { - terminal_image_needs_data( - info.id, - descriptor, - uploaded_images, - oversized_images, - &mut requested_images, - ) + if descriptor.source_file { + return false; + } + terminal_image_needs_data(info.id, descriptor, delivered_images, &mut requested_images) }) { let scrollback_offset = runtime .scroll_metrics() .map(|m| m.offset_from_bottom as u32) .unwrap_or(0); placements.push(HostPlacement { + raw_data: None, pane_id: info.id, host_image_id: None, area: info.inner_rect, @@ -603,97 +436,19 @@ fn collect_visible_placements( fn terminal_image_needs_data( pane_id: PaneId, descriptor: KittyImageDescriptor, - uploaded_images: &HashMap<u32, ImageSignature>, - oversized_images: &HashMap<HostSourceKey, ImageSignature>, + delivered_images: &HashMap<HostSourceKey, ImageSignature>, requested_images: &mut HashSet<(HostSourceKey, ImageSignature)>, ) -> bool { let format_code = kitty_format_code(descriptor.format); let signature = image_signature_from_descriptor(descriptor, format_code); - let host_id = host_image_id_for_signature(pane_id, signature); let source = HostSourceKey::Terminal { pane_id, image_id: descriptor.image_id, }; - uploaded_images.get(&host_id).copied() != Some(signature) - && oversized_images.get(&source).copied() != Some(signature) + delivered_images.get(&source).copied() != Some(signature) && requested_images.insert((source, signature)) } -fn pane_graphics_host_placement( - info: &PaneInfo, - layer_id: &str, - host_id: u32, - cell_size: HostCellSize, - layer: &crate::app::pane_graphics::Layer, - uploaded_images: &HashMap<u32, ImageSignature>, - include_data: bool, -) -> HostPlacement { - let format = pane_graphics_kitty_format(layer.format); - let signature = pane_layer_image_signature(layer); - let data = if !include_data || uploaded_images.get(&host_id).copied() == Some(signature) { - Vec::new() - } else { - layer.inline_data().map(<[u8]>::to_vec).unwrap_or_default() - }; - let render = layer.render; - let grid_cols = if render.grid_cols == 0 { - u32::from(info.inner_rect.width) - } else { - render.grid_cols - }; - let grid_rows = if render.grid_rows == 0 { - u32::from(info.inner_rect.height) - } else { - render.grid_rows - }; - - HostPlacement { - pane_id: info.id, - host_image_id: Some(host_id), - area: info.inner_rect, - cell_size, - source_key: HostSourceKey::PaneLayer { - pane_id: info.id, - layer_id: layer_id.to_owned(), - }, - scrollback_offset: 0, - placement: KittyImagePlacement { - image_id: 1, - placement_id: 1, - z: layer.z_index, - x_offset: 0, - y_offset: 0, - image_width: layer.image_width, - image_height: layer.image_height, - format, - data_len: layer.data_len(), - data_fingerprint: layer.data_fingerprint, - data, - render: KittyPlacementRenderInfo { - pixel_width: layer.image_width, - pixel_height: layer.image_height, - grid_cols, - grid_rows, - viewport_col: render.viewport_col, - viewport_row: render.viewport_row, - source_x: 0, - source_y: 0, - source_width: 0, - source_height: 0, - }, - }, - } -} - -fn pane_graphics_kitty_format(format: crate::api::schema::PaneGraphicsFormat) -> KittyImageFormat { - match format { - crate::api::schema::PaneGraphicsFormat::Png => KittyImageFormat::Png, - crate::api::schema::PaneGraphicsFormat::Rgb => KittyImageFormat::Rgb, - crate::api::schema::PaneGraphicsFormat::Rgba - | crate::api::schema::PaneGraphicsFormat::Bgra => KittyImageFormat::Rgba, - } -} - fn host_image_id(pane_id: PaneId, placement: &KittyImagePlacement) -> u32 { let format_code = kitty_format_code(placement.format); host_image_id_for_signature( @@ -719,11 +474,6 @@ fn host_placement_id(source_key: &HostSourceKey, placement: &KittyImagePlacement let mut hasher = DefaultHasher::new(); match source_key { HostSourceKey::Terminal { pane_id, .. } => pane_id.raw().hash(&mut hasher), - HostSourceKey::PaneLayer { pane_id, layer_id } => { - "pane.graphics".hash(&mut hasher); - pane_id.raw().hash(&mut hasher); - layer_id.hash(&mut hasher); - } HostSourceKey::ClientSurface { scope, source } => { "client.surface".hash(&mut hasher); scope.hash(&mut hasher); @@ -735,11 +485,6 @@ fn host_placement_id(source_key: &HostSourceKey, placement: &KittyImagePlacement 1 + ((hasher.finish() as u32) % 900_000) } -pub(crate) struct DirectFileCommand { - pub(crate) leading: Vec<u8>, - pub(crate) control: String, -} - #[cfg(unix)] pub(crate) fn encode_kitty_regular_file( out: &mut Vec<u8>, @@ -754,7 +499,7 @@ pub(crate) fn encode_kitty_regular_file( out.extend_from_slice(b"\x1b8"); } -fn encode_delete_image(out: &mut Vec<u8>, id: u32) { +pub(crate) fn encode_delete_image(out: &mut Vec<u8>, id: u32) { let _ = write!(out, "\x1b_Ga=d,d=I,i={id},q=2;\x1b\\"); } @@ -765,45 +510,24 @@ fn encode_delete_placement(out: &mut Vec<u8>, host_id: u32, host_placement_id: u ); } -fn encode_upload_image( - out: &mut Vec<u8>, - placement: &HostPlacement, - format_code: u32, - host_id: u32, -) -> bool { - if placement.placement.data.is_empty() { - return false; - } - - let control = format!( +fn upload_control(placement: &HostPlacement, format_code: u32, host_id: u32) -> String { + format!( "a=t,t=d,f={format_code},s={},v={},i={host_id},q=2", placement.placement.image_width, placement.placement.image_height, - ); - encode_kitty_data(out, &control, &placement.placement.data); - true + ) } -fn encode_transmit_and_display( +fn encode_upload_image( out: &mut Vec<u8>, placement: &HostPlacement, - clipped: ClippedPlacement, format_code: u32, host_id: u32, - host_placement_id: u32, ) -> bool { if placement.placement.data.is_empty() { return false; } - let _ = write!(out, "\x1b[{};{}H", clipped.y + 1, clipped.x + 1); - let mut control = format!( - "a=T,t=d,f={format_code},s={},v={},i={host_id},p={host_placement_id},c={},r={},z={},C=1,q=2", - placement.placement.image_width, - placement.placement.image_height, - clipped.cols, - clipped.rows, - placement.placement.z, - ); - append_placement_controls(&mut control, clipped); + + let control = upload_control(placement, format_code, host_id); encode_kitty_data(out, &control, &placement.placement.data); true } @@ -993,16 +717,6 @@ fn scale_pixels(value: u32, source: u32, dest: u32) -> u32 { ((value as u64).saturating_mul(source as u64) / dest.max(1) as u64).min(u32::MAX as u64) as u32 } -fn pane_layer_image_signature(layer: &crate::app::pane_graphics::Layer) -> ImageSignature { - ImageSignature { - image_width: layer.image_width, - image_height: layer.image_height, - format_code: kitty_format_code(pane_graphics_kitty_format(layer.format)), - data_len: layer.data_len(), - data_fingerprint: layer.data_fingerprint, - } -} - fn image_signature(placement: &HostPlacement, format_code: u32) -> ImageSignature { ImageSignature { image_width: placement.placement.image_width, @@ -1056,27 +770,80 @@ fn kitty_format_code(format: KittyImageFormat) -> u32 { } fn encode_kitty_data(out: &mut Vec<u8>, control: &str, data: &[u8]) { + write_kitty_data(out, control, data).expect("writing to Vec cannot fail"); +} + +pub(crate) fn write_kitty_data( + out: &mut impl Write, + control: &str, + data: &[u8], +) -> std::io::Result<()> { let mut chunks = data.chunks(KITTY_CHUNK_BYTES).peekable(); let Some(first) = chunks.next() else { - return; + return Ok(()); }; let more = if chunks.peek().is_some() { 1 } else { 0 }; let encoded = base64::engine::general_purpose::STANDARD.encode(first); - let _ = write!(out, "\x1b_G{control},m={more};{encoded}\x1b\\"); + write!(out, "\x1b_G{control},m={more};{encoded}\x1b\\")?; while let Some(chunk) = chunks.next() { let more = if chunks.peek().is_some() { 1 } else { 0 }; let encoded = base64::engine::general_purpose::STANDARD.encode(chunk); - let _ = write!(out, "\x1b_Gm={more};{encoded}\x1b\\"); + write!(out, "\x1b_Gm={more};{encoded}\x1b\\")?; } + Ok(()) } #[cfg(test)] mod tests { use super::*; + #[test] + fn deferred_updates_match_inline_for_rgb_and_rgba() { + for format in [KittyImageFormat::Rgb, KittyImageFormat::Rgba] { + let mut first = test_placement(0, 0); + first.placement.format = format; + first.placement.data = vec![37; KITTY_CHUNK_BYTES * 2 + 7]; + first.placement.data_len = first.placement.data.len(); + let data: Arc<[u8]> = Arc::from(first.placement.data.clone()); + let mut second = test_placement(3, 0); + second.placement = first.placement.clone(); + second.placement.placement_id += 1; + let mut placements = [first, second]; + let mut inline_cache = HostGraphicsCache::default(); + let expected = drain_graphics_updates(&mut inline_cache, &placements); + for placement in &mut placements { + placement.placement.data.clear(); + placement.raw_data = Some(Arc::clone(&data)); + } + let mut cache = HostGraphicsCache::default(); + let mut output = GraphicsOutput::default(); + loop { + let (next, incomplete) = + encode_graphics_output_incremental(&mut cache, &placements); + output.extend(next); + if !incomplete { + break; + } + } + assert_eq!(output.clone().into_inline_bytes(), expected); + let uploads = output + .operations + .iter() + .filter_map(|op| match op { + GraphicsOperation::Upload { data, .. } => Some(data), + _ => None, + }) + .collect::<Vec<_>>(); + assert_eq!(uploads.len(), 1); + assert!(Arc::ptr_eq(uploads[0], &data)); + assert_eq!(cache.placements.len(), 2); + } + } + fn test_placement(viewport_col: i32, viewport_row: i32) -> HostPlacement { HostPlacement { + raw_data: None, pane_id: PaneId::from_raw(1), host_image_id: None, area: Rect::new(0, 0, 20, 10), @@ -1101,7 +868,8 @@ mod tests { data_len: 30 * 30 * 4, data_fingerprint: 42, data: vec![255; 30 * 30 * 4], - render: KittyPlacementRenderInfo { + source_file: None, + render: crate::ghostty::KittyPlacementRenderInfo { pixel_width: 0, pixel_height: 0, grid_cols: 3, @@ -1117,15 +885,6 @@ mod tests { } } - fn pane_layer_placement(viewport_col: i32, viewport_row: i32) -> HostPlacement { - let mut placement = test_placement(viewport_col, viewport_row); - placement.source_key = HostSourceKey::PaneLayer { - pane_id: placement.pane_id, - layer_id: "primary".into(), - }; - placement - } - fn update( cache: &mut HostGraphicsCache, placements: &[HostPlacement], @@ -1135,13 +894,7 @@ mod tests { if replay { cache.request_placement_replay(); } - let live = cache - .sources - .keys() - .filter(|source| matches!(source, HostSourceKey::PaneLayer { .. })) - .cloned() - .collect::<HashSet<_>>(); - bytes.extend(drain_graphics_updates(cache, placements, &live)); + bytes.extend(drain_graphics_updates(cache, placements)); bytes } @@ -1158,16 +911,6 @@ mod tests { host_placement_id(&placement.source_key, &placement.placement), expected ); - assert_ne!( - host_placement_id( - &HostSourceKey::PaneLayer { - pane_id: placement.pane_id, - layer_id: "primary".into(), - }, - &placement.placement, - ), - expected - ); } #[cfg(unix)] @@ -1187,33 +930,6 @@ mod tests { } #[test] - fn pane_graphics_image_ids_are_disjoint_from_terminal_image_ids() { - let placement = test_placement(0, 0); - let signature = image_signature(&placement, kitty_format_code(placement.placement.format)); - let terminal_id = host_image_id_for_signature(placement.pane_id, signature); - let mut graphics = crate::app::pane_graphics::Runtime::default(); - let primary = (placement.pane_id, "primary".into()); - let pane_graphics_id = graphics.reserve_image_id(&primary).unwrap(); - graphics.slots.insert( - primary.clone(), - crate::app::pane_graphics::Slot::test(pane_graphics_id, None), - ); - - assert_eq!(terminal_id & PANE_GRAPHICS_IMAGE_ID_BIT, 0); - assert_ne!(pane_graphics_id & PANE_GRAPHICS_IMAGE_ID_BIT, 0); - assert_eq!( - pane_graphics_id, - graphics.reserve_image_id(&primary).unwrap() - ); - assert_ne!( - pane_graphics_id, - graphics - .reserve_image_id(&(placement.pane_id, "toolbar".into())) - .unwrap() - ); - } - - #[test] fn clipped_placement_handles_positive_viewport_without_wrapping() { let placement = test_placement(2, 2); let (clipped, _) = clipped_placement(&placement).expect("visible placement"); @@ -1240,47 +956,6 @@ mod tests { } #[test] - fn pane_graphics_layer_defaults_to_full_pane_grid() { - let info = PaneInfo { - id: PaneId::from_raw(9), - rect: Rect::new(0, 0, 12, 5), - inner_rect: Rect::new(2, 1, 8, 3), - scrollbar_rect: None, - borders: ratatui::widgets::Borders::NONE, - is_focused: true, - }; - let layer = crate::app::pane_graphics::Layer::inline( - crate::api::schema::PaneGraphicsFormat::Rgba, - 80, - 30, - vec![255; 80 * 30 * 4], - crate::api::schema::PaneGraphicsPlacementParams::default(), - 0, - ); - - let placement = pane_graphics_host_placement( - &info, - "primary", - PANE_GRAPHICS_IMAGE_ID_BIT | 1, - HostCellSize { - width_px: 10, - height_px: 10, - }, - &layer, - &HashMap::new(), - true, - ); - let (clipped, format_code) = clipped_placement(&placement).expect("visible layer"); - - assert_eq!(format_code, 32); - assert_eq!(clipped.x, 2); - assert_eq!(clipped.y, 1); - assert_eq!(clipped.cols, 8); - assert_eq!(clipped.rows, 3); - assert_eq!(placement.placement.data.len(), 80 * 30 * 4); - } - - #[test] fn graphics_update_uploads_once_then_repositions_only() { let mut cache = HostGraphicsCache::default(); let first = update(&mut cache, &[test_placement(0, 0)], false); @@ -1335,30 +1010,6 @@ mod tests { #[test] fn changing_first_source_does_not_starve_second_source() { - let layers = |first| { - [(1, "a", first), (2, "b", 80)].map(|(id, name, fingerprint)| { - let mut placement = pane_layer_placement(0, 0); - placement.host_image_id = Some(PANE_GRAPHICS_IMAGE_ID_BIT | id); - placement.source_key = HostSourceKey::PaneLayer { - pane_id: placement.pane_id, - layer_id: name.into(), - }; - placement.placement.data_fingerprint = fingerprint; - placement - }) - }; - let initial = layers(42); - let live = initial.iter().map(|p| p.source_key.clone()).collect(); - let mut cache = HostGraphicsCache::default(); - assert!( - encode_graphics_update_incremental(&mut cache, &initial, &live, None, false).incomplete - ); - assert!( - encode_graphics_update_incremental(&mut cache, &layers(43), &live, None, false) - .incomplete - ); - assert_eq!(cache.images.len(), 2, "second source uploaded next"); - let terminal = |id| { let mut placement = test_placement(0, 0); placement.placement.image_id = id; @@ -1373,57 +1024,14 @@ mod tests { let mut cache = HostGraphicsCache::default(); for id in 1..=3 { assert!( - encode_graphics_update_incremental( - &mut cache, - &[terminal(id), terminal(99)], - &HashSet::new(), - None, - false, - ) - .incomplete + encode_graphics_update_incremental(&mut cache, &[terminal(id), terminal(99)]) + .incomplete ); } assert!(cache.sources.contains_key(&second)); } #[test] - fn large_terminal_image_is_local_but_quarantined_headless() { - let placements = || { - let mut large = test_placement(0, 0); - large.placement.data_len = 24 * 1024 * 1024; - let mut later = test_placement(4, 0); - later.placement.image_id = 8; - later.source_key = HostSourceKey::Terminal { - pane_id: later.pane_id, - image_id: 8, - }; - [large, later] - }; - for (budget, expected) in [ - (None, (true, 1, 0)), - (Some(HEADLESS_GRAPHICS_TRANSACTION_BUDGET), (false, 1, 1)), - ] { - let mut cache = HostGraphicsCache::default(); - let encoded = encode_graphics_update_incremental( - &mut cache, - &placements(), - &HashSet::new(), - budget, - false, - ); - assert!(String::from_utf8_lossy(&encoded.bytes).contains("a=t")); - assert_eq!( - ( - encoded.incomplete, - cache.images.len(), - cache.oversized.len() - ), - expected - ); - } - } - - #[test] fn terminal_image_data_requests_deduplicate_and_reconsider_changed_signatures() { let pane_id = PaneId::from_raw(1); let descriptor = KittyImageDescriptor { @@ -1434,13 +1042,13 @@ mod tests { format: KittyImageFormat::Rgba, data_len: 3456 * 2234 * 4, data_fingerprint: 42, + source_file: false, }; let mut requested = HashSet::new(); assert!(terminal_image_needs_data( pane_id, descriptor, &HashMap::new(), - &HashMap::new(), &mut requested, )); let mut second_placement = descriptor; @@ -1449,7 +1057,6 @@ mod tests { pane_id, second_placement, &HashMap::new(), - &HashMap::new(), &mut requested, )); @@ -1458,13 +1065,12 @@ mod tests { pane_id, image_id: descriptor.image_id, }; - let oversized = HashMap::from([(source, signature)]); + let delivered = HashMap::from([(source, signature)]); let mut requested = HashSet::new(); assert!(!terminal_image_needs_data( pane_id, descriptor, - &HashMap::new(), - &oversized, + &delivered, &mut requested, )); let mut changed = descriptor; @@ -1472,38 +1078,8 @@ mod tests { assert!(terminal_image_needs_data( pane_id, changed, - &HashMap::new(), - &oversized, + &delivered, &mut requested, )); } - - #[test] - fn maximum_pane_graphics_stream_payload_fits_client_graphics_frame() { - let mut placement = pane_layer_placement(0, 0); - placement.placement.format = KittyImageFormat::Png; - placement.placement.image_width = 1; - placement.placement.image_height = 1; - placement.placement.data = vec![1_u8; crate::api::schema::PANE_GRAPHICS_STREAM_MAX_BYTES]; - placement.placement.data_len = placement.placement.data.len(); - let (clipped, format_code) = clipped_placement(&placement).expect("visible placement"); - let host_id = host_image_id(placement.pane_id, &placement.placement); - let mut encoded = Vec::new(); - - assert!(encode_upload_image( - &mut encoded, - &placement, - format_code, - host_id, - )); - encode_display_placement(&mut encoded, clipped, host_id, 1, 0); - - let mut framed = Vec::new(); - crate::protocol::write_message( - &mut framed, - &crate::protocol::ServerMessage::Graphics { bytes: encoded }, - ) - .unwrap(); - assert!(framed.len() <= crate::protocol::MAX_GRAPHICS_FRAME_SIZE + 4); - } } |
