diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-24 20:59:48 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-24 20:59:48 +0300 |
| commit | c411883ec639c9893ed9c33021c890485e91727b (patch) | |
| tree | 797bb6efdb3a9425e3d8c31953f62452bd564cb1 /src/app | |
| parent | 11a7ab4ae21ab9427bcb44b3be01f62a4ab58c47 (diff) | |
feat!: replace the custom graphics api with optimized native kitty rendering (#4561)
* feat!: replace the custom graphics api with optimized native kitty rendering
* fix: enable safe kitty optimizations automatically
---------
Co-authored-by: JJ Liebig <jonathan.liebig@gmail.com>
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/api.rs | 29 | ||||
| -rw-r--r-- | src/app/api/pane_graphics.rs | 1284 | ||||
| -rw-r--r-- | src/app/mod.rs | 7 | ||||
| -rw-r--r-- | src/app/pane_graphics.rs | 350 |
4 files changed, 0 insertions, 1670 deletions
diff --git a/src/app/api.rs b/src/app/api.rs index c56a07a1..62c8a6b5 100644 --- a/src/app/api.rs +++ b/src/app/api.rs @@ -5,7 +5,6 @@ mod agents; mod env; mod integrations; mod layouts; -mod pane_graphics; mod panes; pub(crate) mod plugins; pub(super) mod responses; @@ -1152,34 +1151,6 @@ impl App { } Method::PaneRename(params) => return self.handle_pane_rename(request.id, params), Method::PaneRead(params) => return self.handle_pane_read(request.id, params), - Method::PaneGraphicsSet(params) => { - return self.handle_pane_graphics_set(request.id, params); - } - Method::PaneGraphicsClear(params) => { - return self.handle_pane_graphics_clear(request.id, params); - } - Method::PaneGraphicsInfo(params) => { - return self.handle_pane_graphics_info(request.id, params); - } - Method::PaneGraphicsStream(_) => { - return responses::encode_error( - request.id, - "stream_transport_required", - "pane.graphics.stream requires the streaming socket transport", - ); - } - Method::PaneGraphicsStreamSet(params) => { - return self.handle_pane_graphics_stream_set(request.id, params); - } - Method::PaneGraphicsStreamDirect(params) => { - return self.handle_pane_graphics_stream_direct(request.id, params); - } - Method::PaneGraphicsStreamOpen(params) => { - return self.handle_pane_graphics_stream_open(request.id, params); - } - Method::PaneGraphicsStreamClose(params) => { - return self.handle_pane_graphics_stream_close(request.id, params); - } Method::PaneReportAgent(params) => { return self.handle_pane_report_agent(request.id, params); } diff --git a/src/app/api/pane_graphics.rs b/src/app/api/pane_graphics.rs deleted file mode 100644 index 649282b5..00000000 --- a/src/app/api/pane_graphics.rs +++ /dev/null @@ -1,1284 +0,0 @@ -use base64::Engine; - -use crate::api::schema::{ - PaneGraphicsClearParams, PaneGraphicsSetParams, PaneGraphicsStreamParams, ResponseResult, - PANE_GRAPHICS_DIRECT_FILE_MAX_BYTES, PANE_GRAPHICS_MAX_LAYERS_PER_PANE, - PANE_GRAPHICS_PRIMARY_LAYER_ID, PANE_GRAPHICS_SET_MAX_BYTES, PANE_GRAPHICS_STREAM_MAX_BYTES, -}; -use crate::app::pane_graphics::{Key as PaneGraphicsKey, Layer, Slot}; -use crate::app::App; -use crate::layout::PaneId; - -use super::responses::{encode_error, encode_success}; - -impl App { - /// Whether graphics for a pane can be placed on the surface rendered by this client. - /// - /// This deliberately follows the pane layout rather than pane existence: inactive - /// workspaces/tabs and panes hidden by zoom are not placeable. Short-lived UI modes do not - /// suspend the producer because the pane becomes visible again without a layout event. - fn pane_graphics_visible(&self, ws_idx: usize, pane_id: PaneId) -> bool { - self.state.pane_visible_on_active_surface(ws_idx, pane_id) - } - - pub(super) fn handle_pane_graphics_info( - &mut self, - id: String, - target: crate::api::schema::PaneTarget, - ) -> String { - if let Err(response) = require_enabled(self, &id) { - return response; - } - let Some((ws_idx, pane_id)) = self.parse_pane_id(&target.pane_id) else { - return pane_not_found(id, &target.pane_id); - }; - let pane_visible = self.pane_graphics_visible(ws_idx, pane_id); - if !self.state.host_cell_size.is_known() { - return encode_error(id, "cell_size_unavailable", "host cell size is unavailable"); - } - let file_frame_directory = self - .direct_graphics_available - .then(|| self.pane_graphics_files.source_directory().ok()) - .flatten(); - let direct = file_frame_directory.is_some(); - encode_success( - id, - ResponseResult::PaneGraphicsInfo { - cell_width_px: self.state.host_cell_size.width_px, - cell_height_px: self.state.host_cell_size.height_px, - pane_visible, - file_frame_directory: file_frame_directory - .map(|directory| directory.to_string_lossy().into_owned()), - file_frame_formats: if direct { - vec!["rgba".into(), "bgra".into()] - } else { - Vec::new() - }, - file_frame_max_bytes: direct.then_some(PANE_GRAPHICS_STREAM_MAX_BYTES), - file_frame_direct_max_bytes: direct.then_some(PANE_GRAPHICS_DIRECT_FILE_MAX_BYTES), - // Damage metadata never changes the complete canonical frame contract. - file_frame_damage: true, - max_layers_per_pane: PANE_GRAPHICS_MAX_LAYERS_PER_PANE, - pixel_mouse: self.pixel_mouse_available, - file_frame_transport: direct.then(|| "direct-kitty".into()), - }, - ) - } - - pub(crate) fn attach_pane_graphics_stream_active( - &mut self, - params: &PaneGraphicsStreamParams, - active: std::sync::Arc<std::sync::atomic::AtomicBool>, - response: &str, - ) { - if serde_json::from_str::<crate::api::schema::SuccessResponse>(response).is_err() { - return; - } - let Some((_, pane_id)) = self.parse_pane_id(¶ms.pane_id) else { - return; - }; - let Ok(key) = graphics_key(pane_id, params.layer_id.as_deref()) else { - return; - }; - self.pane_graphics - .attach_stream_active(&key, ¶ms.owner, active); - } - - pub(super) fn handle_pane_graphics_set( - &mut self, - id: String, - params: PaneGraphicsSetParams, - ) -> String { - if let Err(response) = require_enabled(self, &id) { - return response; - } - let Some((_, pane_id)) = self.parse_pane_id(¶ms.pane_id) else { - return pane_not_found(id, ¶ms.pane_id); - }; - let key = match graphics_key(pane_id, params.layer_id.as_deref()) { - Ok(key) => key, - Err(message) => return encode_error(id, "invalid_layer_id", message), - }; - self.reclaim_inactive_stream(&key); - if self - .pane_graphics - .slots - .get(&key) - .is_some_and(|slot| slot.stream_owner.is_some()) - { - return encode_error( - id, - "stream_conflict", - "pane graphics layer has an active stream", - ); - } - self.set_layer(id, key, params) - } - - pub(super) fn handle_pane_graphics_clear( - &mut self, - id: String, - params: PaneGraphicsClearParams, - ) -> String { - if let Err(response) = require_enabled(self, &id) { - return response; - } - let Some((_, pane_id)) = self.parse_pane_id(¶ms.pane_id) else { - return pane_not_found(id, ¶ms.pane_id); - }; - let key = match graphics_key(pane_id, params.layer_id.as_deref()) { - Ok(key) => key, - Err(message) => return encode_error(id, "invalid_layer_id", message), - }; - self.reclaim_inactive_stream(&key); - if self - .pane_graphics - .slots - .get(&key) - .is_some_and(|slot| slot.stream_owner.is_some()) - { - return encode_error( - id, - "stream_conflict", - "pane graphics layer has an active stream", - ); - } - if self.pane_graphics.slots.remove(&key).is_some() { - self.pane_graphics.mark_changed(); - } - encode_success(id, ResponseResult::Ok {}) - } - - pub(super) fn handle_pane_graphics_stream_set( - &mut self, - id: String, - params: PaneGraphicsSetParams, - ) -> String { - if let Err(response) = require_enabled(self, &id) { - return response; - } - let Some((_, pane_id)) = self.parse_pane_id(¶ms.pane_id) else { - return pane_not_found(id, ¶ms.pane_id); - }; - let key = match graphics_key(pane_id, params.layer_id.as_deref()) { - Ok(key) => key, - Err(message) => return encode_error(id, "invalid_layer_id", message), - }; - match self.pane_graphics.slots.get(&key) { - Some(slot) - if slot.stream_owner.as_ref() == Some(¶ms.owner) && slot.stream_is_active() => - { - self.set_layer(id, key, params) - } - Some(slot) if slot.stream_owner.as_ref() == Some(¶ms.owner) => { - encode_error(id, "stream_closed", "pane graphics stream is not active") - } - Some(_) => encode_error( - id, - "stream_conflict", - "pane graphics stream owner does not match active stream", - ), - None => encode_error(id, "stream_closed", "pane graphics stream is not active"), - } - } - - pub(super) fn handle_pane_graphics_stream_open( - &mut self, - id: String, - params: PaneGraphicsStreamParams, - ) -> String { - if let Err(response) = require_enabled(self, &id) { - return response; - } - if params.owner.is_empty() { - return encode_error( - id, - "invalid_stream", - "pane graphics stream owner is required", - ); - } - let Some((_, pane_id)) = self.parse_pane_id(¶ms.pane_id) else { - return pane_not_found(id, ¶ms.pane_id); - }; - let key = match graphics_key(pane_id, params.layer_id.as_deref()) { - Ok(key) => key, - Err(message) => return encode_error(id, "invalid_layer_id", message), - }; - let stale = self - .pane_graphics - .slots - .get(&key) - .is_some_and(|slot| slot.stream_owner.is_some() && !slot.stream_is_active()); - if stale { - self.pane_graphics.slots.remove(&key); - } - if self - .pane_graphics - .slots - .get(&key) - .and_then(|slot| slot.stream_owner.as_ref()) - .is_some() - { - return encode_error( - id, - "stream_conflict", - "pane graphics layer has an active stream", - ); - } - let exists = self.pane_graphics.slots.contains_key(&key); - if !self.pane_graphics.can_add_slot(&key) - || (!exists - && self.pane_graphics.layer_count(pane_id) >= PANE_GRAPHICS_MAX_LAYERS_PER_PANE) - { - return encode_error(id, "layer_limit", "pane graphics layer limit reached"); - } - let Some(host_image_id) = self.pane_graphics.reserve_image_id(&key) else { - return encode_error(id, "layer_limit", "pane graphics image ids are exhausted"); - }; - self.pane_graphics.slots.insert( - key, - Slot { - host_image_id, - layer: None, - stream_owner: Some(params.owner), - stream_active: Some(std::sync::Arc::new(std::sync::atomic::AtomicBool::new( - true, - ))), - direct_gate: None, - }, - ); - self.pane_graphics.mark_changed(); - encode_success(id, ResponseResult::Ok {}) - } - - pub(super) fn handle_pane_graphics_stream_close( - &mut self, - id: String, - params: PaneGraphicsStreamParams, - ) -> String { - let Some((_, pane_id)) = self.parse_pane_id(¶ms.pane_id) else { - return pane_not_found(id, ¶ms.pane_id); - }; - if let Ok(key) = graphics_key(pane_id, params.layer_id.as_deref()) { - if self - .pane_graphics - .slots - .get(&key) - .and_then(|slot| slot.stream_owner.as_ref()) - .is_some_and(|owner| owner == ¶ms.owner) - { - self.pane_graphics.slots.remove(&key); - self.pane_graphics.mark_changed(); - } - } - encode_success(id, ResponseResult::Ok {}) - } - - pub(super) fn handle_pane_graphics_stream_direct( - &mut self, - id: String, - params: crate::api::schema::PaneGraphicsDirectParams, - ) -> String { - if let Err(response) = require_enabled(self, &id) { - return response; - } - let Some((_, pane_id)) = self.parse_pane_id(¶ms.pane_id) else { - return pane_not_found(id, ¶ms.pane_id); - }; - let key = match graphics_key(pane_id, params.layer_id.as_deref()) { - Ok(key) => key, - Err(message) => return encode_error(id, "invalid_layer_id", message), - }; - let owner_matches = self.pane_graphics.slots.get(&key).is_some_and(|slot| { - slot.stream_owner.as_deref() == Some(params.owner.as_str()) && slot.stream_is_active() - }); - if !owner_matches { - return encode_error(id, "stream_closed", "pane graphics stream is not active"); - } - if params.image_width == 0 || params.image_height == 0 { - return encode_error( - id, - "invalid_image", - "image_width and image_height must be greater than zero", - ); - } - if !matches!( - params.format, - crate::api::schema::PaneGraphicsFormat::Rgba - | crate::api::schema::PaneGraphicsFormat::Bgra - ) { - return encode_error(id, "invalid_image", "direct frames require rgba or bgra"); - } - let primary = key.1 == PANE_GRAPHICS_PRIMARY_LAYER_ID; - let direct = self.direct_graphics_available - && primary - && params.format == crate::api::schema::PaneGraphicsFormat::Rgba; - let max_bytes = if direct { - PANE_GRAPHICS_DIRECT_FILE_MAX_BYTES - } else { - PANE_GRAPHICS_STREAM_MAX_BYTES - }; - let expected_len = - match expected_len(params.format, params.image_width, params.image_height) { - Ok(Some(len)) if len <= max_bytes => len, - _ => return encode_error(id, "invalid_image", "invalid direct RGBA dimensions"), - }; - let lease = match self - .pane_graphics_files - .lease(std::path::Path::new(¶ms.path), expected_len) - { - Ok(lease) => lease, - Err(err) => return encode_error(id, "invalid_frame_file", err.to_string()), - }; - if !direct && !self.pane_graphics.can_store_inline(&key, expected_len) { - return encode_error( - id, - "graphics_budget_exceeded", - "pane graphics inline memory limit reached", - ); - } - let layer = if direct { - Layer::direct( - params.image_width, - params.image_height, - lease, - params.placement, - params.z_index, - ) - } else { - let mut data = match lease.copy_rgba() { - Ok(data) => data, - Err(err) => return encode_error(id, "invalid_frame_file", err.to_string()), - }; - canonicalize_bgra(params.format, &mut data); - Layer::inline( - crate::api::schema::PaneGraphicsFormat::Rgba, - params.image_width, - params.image_height, - data, - params.placement, - params.z_index, - ) - }; - let Some(slot) = self.pane_graphics.slots.get_mut(&key) else { - return encode_error(id, "stream_closed", "pane graphics stream is not active"); - }; - if !slot.stream_is_active() { - return encode_error(id, "stream_closed", "pane graphics stream is not active"); - } - slot.layer = Some(layer); - slot.direct_gate = None; - self.pane_graphics.mark_changed(); - encode_success( - id, - ResponseResult::PaneGraphicsFrameAck { - sequence: params.sequence, - revision: params.revision, - }, - ) - } - - fn set_layer( - &mut self, - id: String, - key: PaneGraphicsKey, - params: PaneGraphicsSetParams, - ) -> String { - if !self.pane_graphics.can_add_slot(&key) - || (!self.pane_graphics.slots.contains_key(&key) - && self.pane_graphics.layer_count(key.0) >= PANE_GRAPHICS_MAX_LAYERS_PER_PANE) - { - return encode_error(id, "layer_limit", "pane graphics layer limit reached"); - } - if params.image_width == 0 || params.image_height == 0 { - return encode_error( - id, - "invalid_image", - "image_width and image_height must be greater than zero", - ); - } - let expected = match expected_len(params.format, params.image_width, params.image_height) { - Ok(expected) => expected, - Err(()) => return encode_error(id, "invalid_image", "image dimensions are too large"), - }; - let streamed = params.data.is_some(); - let mut data = match params.data { - Some(data) => data, - None => match base64::engine::general_purpose::STANDARD.decode(params.data_base64) { - Ok(data) => data, - Err(_) => { - return encode_error(id, "invalid_image", "data_base64 is not valid base64") - } - }, - }; - let limit = if streamed { - PANE_GRAPHICS_STREAM_MAX_BYTES - } else { - PANE_GRAPHICS_SET_MAX_BYTES - }; - if data.is_empty() { - return encode_error(id, "invalid_image", "image data must not be empty"); - } - if data.len() > limit { - return encode_error(id, "image_too_large", "image data is too large"); - } - if expected.is_some_and(|size| size != data.len()) { - return encode_error( - id, - "invalid_image", - "image data does not match the frame contract", - ); - } - let format = canonicalize_bgra(params.format, &mut data); - if !self.pane_graphics.can_store_inline(&key, data.len()) { - return encode_error( - id, - "graphics_budget_exceeded", - "pane graphics inline memory limit reached", - ); - } - let Some(host_image_id) = self.pane_graphics.reserve_image_id(&key) else { - return encode_error(id, "layer_limit", "pane graphics image ids are exhausted"); - }; - // Move the liveness handle out before replacing the slot so Slot::drop cannot - // deactivate the stream that the replacement still owns. - let (stream_owner, stream_active) = self - .pane_graphics - .slots - .get_mut(&key) - .map(|slot| (slot.stream_owner.take(), slot.stream_active.take())) - .unwrap_or_default(); - self.pane_graphics.slots.insert( - key, - Slot { - host_image_id, - layer: Some(Layer::inline( - format, - params.image_width, - params.image_height, - data, - params.placement, - params.z_index, - )), - stream_owner, - stream_active, - direct_gate: None, - }, - ); - self.pane_graphics.mark_changed(); - encode_success(id, ResponseResult::Ok {}) - } - - fn reclaim_inactive_stream(&mut self, key: &PaneGraphicsKey) { - let stale = self - .pane_graphics - .slots - .get(key) - .is_some_and(|slot| slot.stream_owner.is_some() && !slot.stream_is_active()); - if stale { - self.pane_graphics.slots.remove(key); - self.pane_graphics.mark_changed(); - } - } -} - -fn canonicalize_bgra( - format: crate::api::schema::PaneGraphicsFormat, - data: &mut [u8], -) -> crate::api::schema::PaneGraphicsFormat { - if format != crate::api::schema::PaneGraphicsFormat::Bgra { - return format; - } - for pixel in data.chunks_exact_mut(4) { - pixel.swap(0, 2); - } - crate::api::schema::PaneGraphicsFormat::Rgba -} - -fn graphics_key(pane_id: PaneId, layer_id: Option<&str>) -> Result<PaneGraphicsKey, &'static str> { - let layer_id = layer_id.unwrap_or(PANE_GRAPHICS_PRIMARY_LAYER_ID); - if layer_id.is_empty() || layer_id.len() > 64 { - return Err("layer_id must contain between 1 and 64 characters"); - } - if !layer_id - .bytes() - .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'-')) - { - return Err("layer_id contains unsupported characters"); - } - Ok((pane_id, layer_id.to_owned())) -} - -fn expected_len( - format: crate::api::schema::PaneGraphicsFormat, - width: u32, - height: u32, -) -> Result<Option<usize>, ()> { - let bytes = match format { - crate::api::schema::PaneGraphicsFormat::Png => return Ok(None), - crate::api::schema::PaneGraphicsFormat::Rgb => 3_u64, - crate::api::schema::PaneGraphicsFormat::Rgba - | crate::api::schema::PaneGraphicsFormat::Bgra => 4_u64, - }; - u64::from(width) - .checked_mul(u64::from(height)) - .and_then(|pixels| pixels.checked_mul(bytes)) - .and_then(|size| usize::try_from(size).ok()) - .map(Some) - .ok_or(()) -} - -fn require_enabled(app: &App, id: &str) -> Result<(), String> { - app.state - .kitty_graphics_enabled - .then_some(()) - .ok_or_else(|| { - encode_error( - id.to_owned(), - "feature_disabled", - "pane graphics are disabled by terminal.kitty_graphics", - ) - }) -} - -fn pane_not_found(id: String, pane_id: &str) -> String { - encode_error(id, "pane_not_found", format!("pane {pane_id} not found")) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::api::schema::{ErrorResponse, SuccessResponse}; - use crate::config::Config; - use crate::workspace::Workspace; - - fn app() -> (App, String) { - let (_tx, rx) = tokio::sync::mpsc::unbounded_channel(); - let mut app = App::new( - &Config::default(), - crate::app::AppPolicy::TEST, - None, - rx, - crate::api::EventHub::default(), - ); - app.state.workspaces = vec![Workspace::test_new("graphics")]; - app.state.ensure_test_terminals(); - let pane = app.state.workspaces[0].tabs[0].root_pane; - let public = app.public_pane_id(0, pane).unwrap(); - app.state.kitty_graphics_enabled = true; - (app, public) - } - - fn frame( - pane_id: String, - layer_id: Option<&str>, - owner: &str, - value: u8, - ) -> PaneGraphicsSetParams { - PaneGraphicsSetParams { - pane_id, - layer_id: layer_id.map(str::to_owned), - z_index: 2, - owner: owner.to_owned(), - format: crate::api::schema::PaneGraphicsFormat::Rgba, - image_width: 1, - image_height: 1, - data: Some(vec![value; 4]), - data_base64: String::new(), - placement: Default::default(), - } - } - - fn pane_visible(response: &str) -> bool { - serde_json::from_str::<serde_json::Value>(response).unwrap()["result"]["pane_visible"] - .as_bool() - .unwrap() - } - - #[test] - fn info_reports_visibility_for_terminal_surface_workspace_tab_and_zoom() { - let (mut app, pane_id) = app(); - app.state.mode = crate::app::Mode::Terminal; - app.state.active = Some(0); - app.state.host_cell_size = crate::kitty_graphics::HostCellSize { - width_px: 10, - height_px: 20, - }; - let visible = app.handle_pane_graphics_info( - "visible".into(), - crate::api::schema::PaneTarget { - pane_id: pane_id.clone(), - }, - ); - assert!(pane_visible(&visible)); - - let hidden_workspace = Workspace::test_new("hidden"); - let hidden_workspace_pane = hidden_workspace.tabs[0].root_pane; - app.state.workspaces.push(hidden_workspace); - let hidden_workspace_id = app.public_pane_id(1, hidden_workspace_pane).unwrap(); - let hidden = app.handle_pane_graphics_info( - "hidden-workspace".into(), - crate::api::schema::PaneTarget { - pane_id: hidden_workspace_id, - }, - ); - assert!(!pane_visible(&hidden)); - - let inactive_tab = app.state.workspaces[0].test_add_tab(Some("inactive")); - let inactive_pane = app.state.workspaces[0].tabs[inactive_tab].root_pane; - let inactive_id = app.public_pane_id(0, inactive_pane).unwrap(); - let hidden_tab = app.handle_pane_graphics_info( - "hidden-tab".into(), - crate::api::schema::PaneTarget { - pane_id: inactive_id, - }, - ); - assert!(!pane_visible(&hidden_tab)); - - app.state.workspaces[0].test_split(ratatui::layout::Direction::Horizontal); - app.state.workspaces[0].tabs[0].zoomed = true; - let zoomed_away = app.handle_pane_graphics_info( - "zoomed-away".into(), - crate::api::schema::PaneTarget { - pane_id: pane_id.clone(), - }, - ); - assert!(!pane_visible(&zoomed_away)); - - app.state.workspaces[0].tabs[0].zoomed = false; - app.state.mode = crate::app::Mode::Navigate; - let short_lived_mode = app.handle_pane_graphics_info( - "navigate".into(), - crate::api::schema::PaneTarget { pane_id }, - ); - assert!(pane_visible(&short_lived_mode)); - } - - #[test] - fn non_persistent_info_keeps_fast_transport_and_exact_pixels_disabled() { - let (mut app, pane_id) = app(); - app.state.host_cell_size = crate::kitty_graphics::HostCellSize { - width_px: 10, - height_px: 20, - }; - assert!(!app.pane_graphics_files.is_initialized()); - let response = app - .handle_pane_graphics_info("info".into(), crate::api::schema::PaneTarget { pane_id }); - let success: SuccessResponse = serde_json::from_str(&response).unwrap(); - assert!(matches!( - success.result, - ResponseResult::PaneGraphicsInfo { - file_frame_damage: true, - max_layers_per_pane: 16, - pixel_mouse: false, - .. - } - )); - assert!(app.pane_graphics.slots.is_empty()); - assert!(!app.pane_graphics_files.is_initialized()); - } - - #[cfg(unix)] - #[test] - fn info_advertises_rgba_direct_and_bgra_fallback_file_formats_on_demand() { - let (mut app, pane_id) = app(); - app.state.host_cell_size = crate::kitty_graphics::HostCellSize { - width_px: 10, - height_px: 20, - }; - app.direct_graphics_available = true; - let response = app - .handle_pane_graphics_info("info".into(), crate::api::schema::PaneTarget { pane_id }); - let value: serde_json::Value = serde_json::from_str(&response).unwrap(); - assert!(app.pane_graphics_files.is_initialized()); - assert_eq!( - value["result"]["file_frame_formats"], - serde_json::json!(["rgba", "bgra"]) - ); - assert_eq!( - value["result"]["file_frame_max_bytes"], - PANE_GRAPHICS_STREAM_MAX_BYTES - ); - assert_eq!( - value["result"]["file_frame_direct_max_bytes"], - PANE_GRAPHICS_DIRECT_FILE_MAX_BYTES - ); - assert_eq!(value["result"]["file_frame_damage"], true); - assert_eq!(value["result"]["file_frame_transport"], "direct-kitty"); - } - - #[test] - fn layers_are_isolated_and_stream_owned() { - let (mut app, pane_id) = app(); - for (layer, z, value) in [(None, 0, 1), (Some("chrome"), 10, 2)] { - let mut request = frame(pane_id.clone(), layer, "", value); - request.z_index = z; - let response = app.handle_pane_graphics_set(format!("set-{value}"), request); - assert!(serde_json::from_str::<SuccessResponse>(&response).is_ok()); - } - let (_, pane) = app.parse_pane_id(&pane_id).unwrap(); - assert_eq!( - app.pane_graphics - .slots - .get(&(pane, PANE_GRAPHICS_PRIMARY_LAYER_ID.into())) - .and_then(|slot| slot.layer.as_ref()) - .unwrap() - .z_index, - 0 - ); - assert_eq!( - app.pane_graphics - .slots - .get(&(pane, "chrome".into())) - .and_then(|slot| slot.layer.as_ref()) - .unwrap() - .z_index, - 10 - ); - let response = app.handle_pane_graphics_stream_open( - "open".into(), - PaneGraphicsStreamParams { - pane_id: pane_id.clone(), - layer_id: Some("chrome".into()), - z_index: 10, - owner: "stream".into(), - }, - ); - assert!(serde_json::from_str::<SuccessResponse>(&response).is_ok()); - let response = - app.handle_pane_graphics_set("conflict".into(), frame(pane_id, Some("chrome"), "", 3)); - let error: ErrorResponse = serde_json::from_str(&response).unwrap(); - assert_eq!(error.error.code, "stream_conflict"); - } - - fn error_code(response: &str) -> String { - serde_json::from_str::<ErrorResponse>(response) - .unwrap() - .error - .code - } - - #[test] - fn validates_layer_and_frame_contracts() { - let cases = [ - (Some("bad space"), 1, 1, vec![1; 4], "invalid_layer_id"), - (None, 0, 1, vec![1; 4], "invalid_image"), - (None, 1, 1, Vec::new(), "invalid_image"), - (None, 1, 1, vec![1; 3], "invalid_image"), - ]; - for (layer, width, height, data, expected) in cases { - let (mut app, pane_id) = app(); - let mut params = frame(pane_id, layer, "", 1); - params.image_width = width; - params.image_height = height; - params.data = Some(data); - let response = app.handle_pane_graphics_set("invalid".into(), params); - assert_eq!(error_code(&response), expected); - assert!(app.pane_graphics.slots.is_empty()); - } - } - - #[test] - fn disabled_requests_have_no_graphics_side_effects() { - let (mut app, pane_id) = app(); - app.state.kitty_graphics_enabled = false; - let revision = app.pane_graphics.revision(); - - let response = app.handle_pane_graphics_set("disabled".into(), frame(pane_id, None, "", 1)); - - assert_eq!(error_code(&response), "feature_disabled"); - assert!(app.pane_graphics.slots.is_empty()); - assert_eq!(app.pane_graphics.revision(), revision); - } - - #[test] - fn stream_owner_controls_only_its_named_layer() { - let (mut app, pane_id) = app(); - let params = |owner: &str| PaneGraphicsStreamParams { - pane_id: pane_id.clone(), - layer_id: Some("chrome".into()), - z_index: 7, - owner: owner.into(), - }; - assert!(serde_json::from_str::<SuccessResponse>( - &app.handle_pane_graphics_stream_open("open".into(), params("owner")) - ) - .is_ok()); - assert_eq!( - error_code(&app.handle_pane_graphics_stream_set( - "wrong".into(), - frame(pane_id.clone(), Some("chrome"), "other", 2), - )), - "stream_conflict" - ); - assert!( - serde_json::from_str::<SuccessResponse>(&app.handle_pane_graphics_stream_set( - "frame".into(), - frame(pane_id.clone(), Some("chrome"), "owner", 3), - )) - .is_ok() - ); - app.handle_pane_graphics_stream_close("stale".into(), params("other")); - assert_eq!(app.pane_graphics.slots.len(), 1); - app.handle_pane_graphics_stream_close("close".into(), params("owner")); - assert!(app.pane_graphics.slots.is_empty()); - } - - #[test] - fn static_set_and_clear_reclaim_inactive_stream_layers() { - let (mut app, pane_id) = app(); - let open = |layer: &str| PaneGraphicsStreamParams { - pane_id: pane_id.clone(), - layer_id: Some(layer.into()), - z_index: 0, - owner: format!("owner-{layer}"), - }; - for layer in ["set", "clear"] { - assert!(serde_json::from_str::<SuccessResponse>( - &app.handle_pane_graphics_stream_open(layer.into(), open(layer)) - ) - .is_ok()); - let (_, pane) = app.parse_pane_id(&pane_id).unwrap(); - app.pane_graphics.slots[&(pane, layer.into())] - .stream_active - .as_ref() - .unwrap() - .store(false, std::sync::atomic::Ordering::Release); - } - - assert!( - serde_json::from_str::<SuccessResponse>(&app.handle_pane_graphics_set( - "set-static".into(), - frame(pane_id.clone(), Some("set"), "", 4), - )) - .is_ok() - ); - assert!( - serde_json::from_str::<SuccessResponse>(&app.handle_pane_graphics_clear( - "clear-static".into(), - PaneGraphicsClearParams { - pane_id, - layer_id: Some("clear".into()), - }, - )) - .is_ok() - ); - assert_eq!(app.pane_graphics.slots.len(), 1); - assert!(app - .pane_graphics - .slots - .values() - .all(|slot| slot.stream_owner.is_none())); - } - - #[test] - fn layer_limit_counts_empty_and_populated_stream_slots() { - let (mut app, pane_id) = app(); - for index in 0..PANE_GRAPHICS_MAX_LAYERS_PER_PANE { - let layer_id = format!("layer-{index}"); - let response = app.handle_pane_graphics_stream_open( - layer_id.clone(), - PaneGraphicsStreamParams { - pane_id: pane_id.clone(), - layer_id: Some(layer_id), - z_index: index as i32, - owner: format!("owner-{index}"), - }, - ); - assert!(serde_json::from_str::<SuccessResponse>(&response).is_ok()); - } - let response = app.handle_pane_graphics_set( - "set-overflow".into(), - frame(pane_id.clone(), Some("set-overflow"), "", 1), - ); - assert_eq!(error_code(&response), "layer_limit"); - let response = app.handle_pane_graphics_stream_open( - "overflow".into(), - PaneGraphicsStreamParams { - pane_id, - layer_id: Some("overflow".into()), - z_index: 0, - owner: "overflow".into(), - }, - ); - assert_eq!(error_code(&response), "layer_limit"); - assert_eq!(app.pane_graphics.slots.len(), 16); - } - - #[test] - fn set_and_clear_preserve_runtime_only_layer_details() { - let (mut app, pane_id) = app(); - let (_, pane) = app.parse_pane_id(&pane_id).unwrap(); - let mut params = frame(pane_id.clone(), None, "", 9); - params.placement.grid_cols = 10; - params.placement.grid_rows = 4; - assert!(serde_json::from_str::<SuccessResponse>( - &app.handle_pane_graphics_set("set".into(), params) - ) - .is_ok()); - let layer = app - .pane_graphics - .slots - .get(&(pane, PANE_GRAPHICS_PRIMARY_LAYER_ID.into())) - .and_then(|slot| slot.layer.as_ref()) - .unwrap(); - assert_eq!(layer.inline_data(), Some([9; 4].as_slice())); - assert_eq!((layer.render.grid_cols, layer.render.grid_rows), (10, 4)); - assert!(!app.state.session_dirty); - - assert!( - serde_json::from_str::<SuccessResponse>(&app.handle_pane_graphics_clear( - "clear".into(), - PaneGraphicsClearParams { - pane_id, - layer_id: None, - }, - )) - .is_ok() - ); - assert!(app.pane_graphics.slots.is_empty()); - } - - #[test] - fn raw_formats_require_exact_non_overflowing_lengths() { - for (format, exact) in [ - (crate::api::schema::PaneGraphicsFormat::Rgb, 6), - (crate::api::schema::PaneGraphicsFormat::Rgba, 8), - ] { - for (length, expected) in [(exact, "ok"), (exact - 1, "invalid_image")] { - let (mut app, pane_id) = app(); - let mut params = frame(pane_id, None, "", 1); - params.format = format; - params.image_width = 2; - params.data = Some(vec![1; length]); - let response = app.handle_pane_graphics_set("length".into(), params); - if expected == "ok" { - assert!(serde_json::from_str::<SuccessResponse>(&response).is_ok()); - } else { - assert_eq!(error_code(&response), expected); - } - } - let (mut app, pane_id) = app(); - let mut params = frame(pane_id, None, "", 1); - params.format = format; - params.image_width = u32::MAX; - params.image_height = u32::MAX; - assert_eq!( - error_code(&app.handle_pane_graphics_set("overflow".into(), params)), - "invalid_image" - ); - } - } - - #[test] - fn public_frames_validate_base64_and_public_size_limit() { - let (mut app, pane_id) = app(); - let mut params = frame(pane_id.clone(), None, "", 1); - params.format = crate::api::schema::PaneGraphicsFormat::Png; - params.data = None; - params.data_base64 = "not base64".into(); - assert_eq!( - error_code(&app.handle_pane_graphics_set("base64".into(), params)), - "invalid_image" - ); - - let mut params = frame(pane_id, None, "", 1); - params.format = crate::api::schema::PaneGraphicsFormat::Png; - params.data = None; - params.data_base64 = - base64::engine::general_purpose::STANDARD - .encode(vec![1; PANE_GRAPHICS_SET_MAX_BYTES + 1]); - assert_eq!( - error_code(&app.handle_pane_graphics_set("large".into(), params)), - "image_too_large" - ); - } - - #[test] - fn aliases_resolve_to_the_same_layer_identity() { - let (mut app, pane_id) = app(); - let (_, pane) = app.parse_pane_id(&pane_id).unwrap(); - let alias = format!("{pane_id}:alias"); - app.state.public_pane_id_aliases.insert(alias.clone(), pane); - let params = |target: String, owner: &str| PaneGraphicsStreamParams { - pane_id: target, - layer_id: None, - z_index: 0, - owner: owner.into(), - }; - assert!(serde_json::from_str::<SuccessResponse>( - &app.handle_pane_graphics_stream_open("open".into(), params(pane_id.clone(), "owner"),) - ) - .is_ok()); - assert_eq!( - error_code(&app.handle_pane_graphics_stream_open( - "conflict".into(), - params(alias.clone(), "other"), - )), - "stream_conflict" - ); - app.handle_pane_graphics_stream_close("stale".into(), params(alias.clone(), "other")); - assert_eq!(app.pane_graphics.slots.len(), 1); - app.handle_pane_graphics_stream_close("close".into(), params(alias, "owner")); - assert!(app.pane_graphics.slots.is_empty()); - } - - #[cfg(unix)] - fn direct_file(app: &App, name: &str, data: &[u8]) -> String { - use std::io::Write as _; - use std::os::unix::fs::OpenOptionsExt as _; - let path = app - .pane_graphics_files - .source_directory() - .unwrap() - .join(name); - let mut file = std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .mode(0o600) - .open(&path) - .unwrap(); - file.write_all(data).unwrap(); - path.to_string_lossy().into_owned() - } - - #[cfg(unix)] - fn sparse_direct_file(app: &App, name: &str, len: usize) -> String { - use std::os::unix::fs::OpenOptionsExt as _; - let path = app - .pane_graphics_files - .source_directory() - .unwrap() - .join(name); - let file = std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .mode(0o600) - .open(&path) - .unwrap(); - file.set_len(len as u64).unwrap(); - path.to_string_lossy().into_owned() - } - - #[cfg(unix)] - fn direct_params( - pane_id: String, - owner: &str, - path: String, - ) -> crate::api::schema::PaneGraphicsDirectParams { - crate::api::schema::PaneGraphicsDirectParams { - pane_id, - layer_id: None, - z_index: 0, - owner: owner.into(), - image_width: 1, - image_height: 1, - format: crate::api::schema::PaneGraphicsFormat::Rgba, - path, - sequence: 1, - revision: 1, - placement: Default::default(), - } - } - - #[cfg(unix)] - #[test] - fn direct_file_is_leased_or_safely_copied_for_inline_fallback() { - let (mut app, pane_id) = app(); - let stream = PaneGraphicsStreamParams { - pane_id: pane_id.clone(), - layer_id: None, - z_index: 0, - owner: "owner".into(), - }; - app.handle_pane_graphics_stream_open("open".into(), stream.clone()); - for (width, height) in [(0, 1), (1, 0)] { - let mut params = direct_params(pane_id.clone(), "owner", "unused".into()); - (params.image_width, params.image_height) = (width, height); - assert_eq!( - error_code(&app.handle_pane_graphics_stream_direct("invalid".into(), params)), - "invalid_image" - ); - } - for format in [ - crate::api::schema::PaneGraphicsFormat::Rgb, - crate::api::schema::PaneGraphicsFormat::Png, - ] { - let mut params = direct_params(pane_id.clone(), "owner", "unused".into()); - params.format = format; - assert_eq!( - error_code(&app.handle_pane_graphics_stream_direct("invalid".into(), params)), - "invalid_image" - ); - } - app.direct_graphics_available = true; - let path = direct_file(&app, "direct", &[1, 2, 3, 4]); - let response = app.handle_pane_graphics_stream_direct( - "direct".into(), - crate::api::schema::PaneGraphicsDirectParams { - sequence: 7, - revision: 8, - ..direct_params(pane_id.clone(), "owner", path) - }, - ); - let ack: SuccessResponse = serde_json::from_str(&response).unwrap(); - assert!(matches!( - ack.result, - ResponseResult::PaneGraphicsFrameAck { - sequence: 7, - revision: 8 - } - )); - assert!(app - .pane_graphics - .slots - .values() - .next() - .unwrap() - .layer - .as_ref() - .unwrap() - .direct_lease() - .is_some()); - - app.handle_pane_graphics_stream_close("close".into(), stream.clone()); - app.handle_pane_graphics_stream_open("reopen".into(), stream); - app.direct_graphics_available = false; - let path = direct_file(&app, "fallback", &[5, 6, 7, 8]); - app.handle_pane_graphics_stream_direct( - "fallback".into(), - crate::api::schema::PaneGraphicsDirectParams { - sequence: 9, - revision: 10, - ..direct_params(pane_id, "owner", path) - }, - ); - assert_eq!( - app.pane_graphics - .slots - .values() - .next() - .unwrap() - .layer - .as_ref() - .unwrap() - .inline_data(), - Some([5, 6, 7, 8].as_slice()) - ); - } - - #[cfg(unix)] - #[test] - fn direct_primary_rgba_accepts_fullscreen_retina_frame() { - let (mut app, pane_id) = app(); - app.direct_graphics_available = true; - app.handle_pane_graphics_stream_open( - "open".into(), - PaneGraphicsStreamParams { - pane_id: pane_id.clone(), - layer_id: None, - z_index: 0, - owner: "owner".into(), - }, - ); - let (image_width, image_height) = (3456, 2234); - let len = image_width * image_height * 4; - let path = sparse_direct_file(&app, "retina-frame", len as usize); - let response = app.handle_pane_graphics_stream_direct( - "frame".into(), - crate::api::schema::PaneGraphicsDirectParams { - image_width, - image_height, - ..direct_params(pane_id, "owner", path) - }, - ); - - assert!(serde_json::from_str::<SuccessResponse>(&response).is_ok()); - assert!(app - .pane_graphics - .slots - .values() - .next() - .unwrap() - .layer - .as_ref() - .unwrap() - .direct_lease() - .is_some()); - } - - #[cfg(unix)] - #[test] - fn bgra_and_secondary_file_frames_are_canonical_owned_rgba() { - let (mut app, pane_id) = app(); - app.direct_graphics_available = true; - for (layer, format, source) in [ - ( - "browser-chrome", - crate::api::schema::PaneGraphicsFormat::Rgba, - [1, 2, 3, 4], - ), - ( - "primary", - crate::api::schema::PaneGraphicsFormat::Bgra, - [3, 2, 1, 4], - ), - ] { - app.handle_pane_graphics_stream_open( - format!("open-{layer}"), - PaneGraphicsStreamParams { - pane_id: pane_id.clone(), - layer_id: Some(layer.into()), - z_index: 0, - owner: layer.into(), - }, - ); - let path = direct_file(&app, layer, &source); - app.handle_pane_graphics_stream_direct( - format!("frame-{layer}"), - crate::api::schema::PaneGraphicsDirectParams { - layer_id: Some(layer.into()), - format, - ..direct_params(pane_id.clone(), layer, path) - }, - ); - let key = (app.parse_pane_id(&pane_id).unwrap().1, layer.into()); - assert_eq!( - app.pane_graphics.slots[&key] - .layer - .as_ref() - .unwrap() - .inline_data(), - Some([1, 2, 3, 4].as_slice()) - ); - } - } - - #[test] - fn disabled_info_and_stream_requests_are_rejected() { - let (mut app, pane_id) = app(); - app.state.kitty_graphics_enabled = false; - assert_eq!( - error_code(&app.handle_pane_graphics_info( - "info".into(), - crate::api::schema::PaneTarget { - pane_id: pane_id.clone(), - }, - )), - "feature_disabled" - ); - assert_eq!( - error_code(&app.handle_pane_graphics_stream_open( - "open".into(), - PaneGraphicsStreamParams { - pane_id, - layer_id: None, - z_index: 0, - owner: "owner".into(), - }, - )), - "feature_disabled" - ); - assert!(app.pane_graphics.slots.is_empty()); - } -} diff --git a/src/app/mod.rs b/src/app/mod.rs index 03e89026..e358a562 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -17,7 +17,6 @@ mod creation; mod custom_commands; mod git_refresh; mod ids; -pub(crate) mod pane_graphics; mod popup; mod runtime; mod session; @@ -103,9 +102,6 @@ impl AppPolicy { pub struct App { pub state: AppState, - pub(crate) pane_graphics: pane_graphics::Runtime, - pub(crate) pane_graphics_files: Arc<crate::pane_graphics_files::FileStore>, - pub(crate) direct_graphics_available: bool, pub(crate) pixel_mouse_available: bool, pub(crate) terminal_runtimes: crate::terminal::TerminalRuntimeRegistry, pub event_tx: mpsc::Sender<AppEvent>, @@ -575,9 +571,6 @@ impl App { toast_deadline: None, last_api_notification_at: None, state, - pane_graphics: pane_graphics::Runtime::default(), - pane_graphics_files: Arc::new(crate::pane_graphics_files::FileStore::default()), - direct_graphics_available: false, pixel_mouse_available: false, terminal_runtimes: restored_terminal_runtimes, event_tx, diff --git a/src/app/pane_graphics.rs b/src/app/pane_graphics.rs deleted file mode 100644 index 71f998d8..00000000 --- a/src/app/pane_graphics.rs +++ /dev/null @@ -1,350 +0,0 @@ -use std::collections::HashMap; -use std::hash::{Hash, Hasher}; - -use crate::api::schema::{PaneGraphicsFormat, PaneGraphicsPlacementParams}; -use crate::layout::PaneId; - -pub(crate) type Key = (PaneId, String); - -pub(crate) const DIRECT_DELIVERY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); -pub(crate) const DIRECT_RESPONSE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3); -pub(crate) const DIRECT_OUTER_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(9); - -#[derive(Debug)] -pub(crate) enum Backing { - Inline(Vec<u8>), - Direct(crate::pane_graphics_files::Lease), - Resident { len: usize, client_id: u64 }, -} - -#[derive(Debug)] -pub(crate) struct Layer { - pub(crate) format: PaneGraphicsFormat, - pub(crate) image_width: u32, - pub(crate) image_height: u32, - pub(crate) backing: Backing, - pub(crate) data_fingerprint: u64, - pub(crate) render: PaneGraphicsPlacementParams, - pub(crate) z_index: i32, -} - -#[derive(Debug)] -pub(crate) struct DirectGate { - pub(crate) transfer_id: u64, - pub(crate) image_id: u32, - pub(crate) client_id: u64, - pub(crate) deadline: std::time::Instant, - pub(crate) written: bool, - pub(crate) success_response: String, - pub(crate) respond_to: std::sync::mpsc::Sender<String>, -} - -#[derive(Debug)] -pub(crate) struct Slot { - pub(crate) host_image_id: u32, - pub(crate) layer: Option<Layer>, - pub(crate) stream_owner: Option<String>, - pub(crate) stream_active: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>, - pub(crate) direct_gate: Option<DirectGate>, -} - -impl Slot { - #[cfg(test)] - pub(crate) fn test(host_image_id: u32, layer: Option<Layer>) -> Self { - Self { - host_image_id, - layer, - stream_owner: None, - stream_active: None, - direct_gate: None, - } - } - - pub(crate) fn stream_is_active(&self) -> bool { - self.stream_active - .as_ref() - .is_some_and(|active| active.load(std::sync::atomic::Ordering::Acquire)) - } - - pub(crate) fn direct_client(&self) -> Option<u64> { - self.direct_gate - .as_ref() - .map(|gate| gate.client_id) - .or_else(|| self.layer.as_ref().and_then(Layer::resident_client)) - } -} - -impl Drop for Slot { - fn drop(&mut self) { - if let Some(active) = &self.stream_active { - active.store(false, std::sync::atomic::Ordering::Release); - } - } -} - -#[derive(Debug)] -pub(crate) struct Runtime { - pub(crate) slots: HashMap<Key, Slot>, - revision: u64, - next_host_image_id: u32, -} - -impl Default for Runtime { - fn default() -> Self { - Self { - slots: HashMap::new(), - revision: 0, - next_host_image_id: 1 << 31, - } - } -} - -impl Runtime { - pub(crate) fn revision(&self) -> u64 { - self.revision - } - - pub(crate) fn mark_changed(&mut self) { - self.revision = self.revision.wrapping_add(1); - } - - #[cfg(test)] - pub(crate) fn clear(&mut self) { - self.slots.clear(); - self.mark_changed(); - } - - pub(crate) fn reserve_image_id(&mut self, key: &Key) -> Option<u32> { - if let Some(id) = self.slots.get(key).map(|slot| slot.host_image_id) { - return Some(id); - } - for _ in 0..=self.slots.len() { - let id = self.next_host_image_id | (1 << 31); - self.next_host_image_id = id.wrapping_add(1) | (1 << 31); - if self.slots.values().all(|slot| slot.host_image_id != id) { - return Some(id); - } - } - None - } - - pub(crate) fn layer_count(&self, pane_id: PaneId) -> usize { - self.slots.keys().filter(|(id, _)| *id == pane_id).count() - } - - pub(crate) fn can_add_slot(&self, key: &Key) -> bool { - self.slots.contains_key(key) - || self.slots.len() < crate::api::schema::panes::PANE_GRAPHICS_MAX_LAYERS_TOTAL - } - - pub(crate) fn can_store_inline(&self, key: &Key, len: usize) -> bool { - let replaced = self - .slots - .get(key) - .and_then(|slot| slot.layer.as_ref()) - .and_then(Layer::inline_data) - .map_or(0, <[u8]>::len); - self.inline_bytes() - .saturating_sub(replaced) - .saturating_add(len) - <= crate::api::schema::panes::PANE_GRAPHICS_MAX_INLINE_BYTES_TOTAL - } - - fn inline_bytes(&self) -> usize { - self.slots - .values() - .filter_map(|slot| slot.layer.as_ref()) - .filter_map(Layer::inline_data) - .map(<[u8]>::len) - .sum() - } - - pub(crate) fn attach_stream_active( - &mut self, - key: &Key, - owner: &str, - active: std::sync::Arc<std::sync::atomic::AtomicBool>, - ) { - if let Some(slot) = self - .slots - .get_mut(key) - .filter(|slot| slot.stream_owner.as_deref() == Some(owner)) - { - slot.stream_active = Some(active); - } - } - - pub(crate) fn retain_live_panes(&mut self, state: &crate::app::state::AppState) -> bool { - if self.slots.is_empty() { - return false; - } - let before = self.slots.len(); - self.slots.retain(|(pane_id, _), _| { - state - .workspaces - .iter() - .any(|workspace| workspace.pane_state(*pane_id).is_some()) - }); - if self.slots.len() == before { - return false; - } - self.mark_changed(); - true - } -} - -impl Layer { - pub(crate) fn inline( - format: PaneGraphicsFormat, - image_width: u32, - image_height: u32, - data: Vec<u8>, - render: PaneGraphicsPlacementParams, - z_index: i32, - ) -> Self { - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - data.hash(&mut hasher); - Self { - format, - image_width, - image_height, - backing: Backing::Inline(data), - data_fingerprint: hasher.finish(), - render, - z_index, - } - } - - pub(crate) fn direct( - image_width: u32, - image_height: u32, - lease: crate::pane_graphics_files::Lease, - render: PaneGraphicsPlacementParams, - z_index: i32, - ) -> Self { - let data_fingerprint = lease.fingerprint(); - Self { - format: PaneGraphicsFormat::Rgba, - image_width, - image_height, - backing: Backing::Direct(lease), - data_fingerprint, - render, - z_index, - } - } - - pub(crate) fn data_len(&self) -> usize { - match &self.backing { - Backing::Inline(data) => data.len(), - Backing::Direct(lease) => lease.len(), - Backing::Resident { len, .. } => *len, - } - } - - pub(crate) fn inline_data(&self) -> Option<&[u8]> { - match &self.backing { - Backing::Inline(data) => Some(data), - Backing::Direct(_) | Backing::Resident { .. } => None, - } - } - - pub(crate) fn direct_lease(&self) -> Option<&crate::pane_graphics_files::Lease> { - match &self.backing { - Backing::Direct(lease) => Some(lease), - Backing::Inline(_) | Backing::Resident { .. } => None, - } - } - - pub(crate) fn mark_resident(&mut self, client_id: u64) -> bool { - let Backing::Direct(lease) = &self.backing else { - return false; - }; - self.backing = Backing::Resident { - len: lease.len(), - client_id, - }; - true - } - - pub(crate) fn resident_client(&self) -> Option<u64> { - match self.backing { - Backing::Resident { client_id, .. } => Some(client_id), - Backing::Inline(_) | Backing::Direct(_) => None, - } - } - - pub(crate) fn terminal_only(&self) -> bool { - matches!(self.backing, Backing::Direct(_) | Backing::Resident { .. }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn stream_liveness_attaches_only_to_the_exact_owned_layer() { - let mut runtime = Runtime::default(); - let first = (PaneId::from_raw(1), "first".into()); - let second = (PaneId::from_raw(2), "second".into()); - for (key, id) in [(&first, 1), (&second, 2)] { - runtime.slots.insert( - key.clone(), - Slot { - host_image_id: (1 << 31) | id, - layer: None, - stream_owner: Some("shared-owner".into()), - stream_active: None, - direct_gate: None, - }, - ); - } - let active = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); - - runtime.attach_stream_active(&second, "shared-owner", active.clone()); - - assert!(runtime.slots[&first].stream_active.is_none()); - assert!(std::sync::Arc::ptr_eq( - runtime.slots[&second].stream_active.as_ref().unwrap(), - &active - )); - } - - #[test] - fn runtime_bounds_total_slots_and_inline_bytes() { - let mut runtime = Runtime::default(); - for index in 0..crate::api::schema::panes::PANE_GRAPHICS_MAX_LAYERS_TOTAL { - let key = (PaneId::from_raw(1), format!("layer-{index}")); - runtime - .slots - .insert(key, Slot::test((1 << 31) | index as u32, None)); - } - assert!(!runtime.can_add_slot(&(PaneId::from_raw(2), "extra".into()))); - - let mut runtime = Runtime::default(); - let key = (PaneId::from_raw(1), "primary".into()); - runtime.slots.insert( - key.clone(), - Slot::test( - 1 << 31, - Some(Layer::inline( - PaneGraphicsFormat::Rgba, - 1, - 1, - vec![0; 4], - PaneGraphicsPlacementParams::default(), - 0, - )), - ), - ); - assert!(runtime.can_store_inline( - &key, - crate::api::schema::panes::PANE_GRAPHICS_MAX_INLINE_BYTES_TOTAL, - )); - assert!(!runtime.can_store_inline( - &(PaneId::from_raw(2), "primary".into()), - crate::api::schema::panes::PANE_GRAPHICS_MAX_INLINE_BYTES_TOTAL, - )); - } -} |
