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.rs688
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);
- }
}