diff options
Diffstat (limited to 'src/kitty_graphics/surface.rs')
| -rw-r--r-- | src/kitty_graphics/surface.rs | 790 |
1 files changed, 680 insertions, 110 deletions
diff --git a/src/kitty_graphics/surface.rs b/src/kitty_graphics/surface.rs index 4df05ffb..9e03d642 100644 --- a/src/kitty_graphics/surface.rs +++ b/src/kitty_graphics/surface.rs @@ -1,10 +1,13 @@ use std::collections::{HashMap, HashSet}; use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use super::output::GraphicsOutput; use ratatui::layout::Rect; use super::{ - clipped_placement, collect_visible_placements, encode_graphics_update_incremental, + clipped_placement, collect_visible_placements, encode_graphics_output_incremental, HostCellSize, HostGraphicsCache, HostPlacement, HostSourceKey, ImageSignature, }; use crate::ghostty::{ @@ -24,7 +27,15 @@ pub(crate) struct DeliveryCache { pending: bool, } +pub(crate) type SourceFiles = + HashMap<SurfaceGraphicsAssetKey, Arc<crate::pane_graphics_files::OwnedExport>>; + impl DeliveryCache { + pub(crate) fn forget_asset(&mut self, key: &SurfaceGraphicsAssetKey) { + self.assets.remove(key); + self.pending = true; + } + pub(crate) fn has_pending(&self) -> bool { self.pending } @@ -92,11 +103,13 @@ impl Occlusion { } } -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] pub(crate) struct ClientState { scope: String, scene: SurfaceGraphicsScene, - assets: HashMap<SurfaceGraphicsAssetKey, Vec<u8>>, + display_scene: SurfaceGraphicsScene, + native_image_ids: HashMap<SurfaceGraphicsAssetKey, u32>, + assets: HashMap<SurfaceGraphicsAssetKey, Arc<[u8]>>, host: HostGraphicsCache, trusted_direct: HashMap<SurfaceGraphicsAssetKey, u32>, reset_pending: bool, @@ -105,6 +118,21 @@ pub(crate) struct ClientState { } impl ClientState { + pub(crate) fn new() -> Self { + Self::default() + } + + fn image_id(&self, key: &SurfaceGraphicsAssetKey) -> u32 { + if matches!(key.source, SurfaceGraphicsSource::Terminal { .. }) { + return self + .native_image_ids + .get(key) + .copied() + .unwrap_or_else(|| native_host_image_id(&self.scope, key)); + } + host_image_id(&self.scope, key) + } + pub(crate) fn scope(&self) -> &str { &self.scope } @@ -115,6 +143,8 @@ impl ClientState { } self.scope = scope.to_owned(); self.scene = SurfaceGraphicsScene::default(); + self.display_scene = SurfaceGraphicsScene::default(); + self.native_image_ids.clear(); self.assets.clear(); self.trusted_direct.clear(); self.stale_images.clear(); @@ -123,17 +153,53 @@ impl ClientState { } #[cfg(unix)] + pub(crate) fn accepts_direct_asset( + &self, + key: &SurfaceGraphicsAssetKey, + image_id: u32, + ) -> bool { + if matches!(key.source, SurfaceGraphicsSource::Terminal { .. }) { + return !self.stale_images.contains(&image_id) + && !self.forced_delete_images.contains(&image_id) + && (image_id == native_host_image_id(&self.scope, key) + || image_id == (native_host_image_id(&self.scope, key) ^ NATIVE_SLOT_BIT)) + && !self + .display_scene + .placements + .iter() + .map(|p| &p.asset) + .chain(self.display_scene.retained_assets.iter()) + .any(|visible| { + self.image_id(visible) == image_id + && self.host.images.get(&image_id) + == Some(&image_signature_from_asset(visible)) + }) + && (self + .scene + .placements + .iter() + .any(|placement| &placement.asset == key) + || self.scene.retained_assets.contains(key)); + } + image_id == host_image_id(&self.scope, key) + } + + #[cfg(unix)] pub(crate) fn trust_direct_asset( &mut self, key: &SurfaceGraphicsAssetKey, image_id: u32, ) -> bool { - if image_id != host_image_id(&self.scope, key) { + if !self.accepts_direct_asset(key, image_id) { return false; } + if matches!(key.source, SurfaceGraphicsSource::Terminal { .. }) { + self.native_image_ids.insert(key.clone(), image_id); + } self.host .images .insert(image_id, image_signature_from_asset(key)); + self.refresh_display_scene(); if self .scene .placements @@ -182,18 +248,6 @@ impl ClientState { .map(|placement| placement.asset.clone()) .chain(scene.retained_assets.iter().cloned()) .collect::<HashSet<_>>(); - let previous = self - .scene - .placements - .iter() - .map(|placement| placement.asset.clone()) - .chain(self.scene.retained_assets.iter().cloned()) - .collect::<HashSet<_>>(); - self.stale_images.extend( - previous - .difference(&desired) - .map(|key| host_image_id(&self.scope, key)), - ); let unclaimed = self .trusted_direct .iter() @@ -206,15 +260,107 @@ impl ClientState { .iter() .map(|placement| placement.asset.clone()) .collect::<HashSet<_>>(); - self.assets.retain(|key, _| placed.contains(key)); for asset in std::mem::take(&mut scene.assets) { if asset.data.len() as u64 == asset.key.data_len && placed.contains(&asset.key) { - self.assets.insert(asset.key, asset.data); + self.assets.insert(asset.key, Arc::from(asset.data)); } } self.scene = scene; + self.refresh_display_scene(); } + fn refresh_display_scene(&mut self) { + let mut next = self.scene.clone(); + let previous_placements = self + .display_scene + .placements + .iter() + .map(|p| ((&p.asset.source, p.logical_placement_id, p.x, p.y), p)) + .collect::<HashMap<_, _>>(); + let previous_assets = self + .display_scene + .retained_assets + .iter() + .chain(self.display_scene.placements.iter().map(|p| &p.asset)) + .filter(|key| { + self.host.images.get(&self.image_id(key)) == Some(&image_signature_from_asset(key)) + }) + .map(|key| (&key.source, key)) + .collect::<HashMap<_, _>>(); + for placement in &mut next.placements { + let key = &placement.asset; + if !matches!(key.source, SurfaceGraphicsSource::Terminal { .. }) + || self.assets.contains_key(key) + || self.host.images.get(&self.image_id(key)) + == Some(&image_signature_from_asset(key)) + { + continue; + } + // A metadata-only replacement is not visible until its upload is ACKed. + // Reuse only an identical placement, never stale resize/scroll geometry. + if let Some(previous) = previous_placements + .get(&( + &key.source, + placement.logical_placement_id, + placement.x, + placement.y, + )) + .copied() + .filter(|previous| { + if self.host.images.get(&self.image_id(&previous.asset)) + != Some(&image_signature_from_asset(&previous.asset)) + { + return false; + } + let mut candidate = (*previous).clone(); + candidate.asset = key.clone(); + candidate == *placement + }) + { + placement.asset = previous.asset.clone(); + } + } + for key in &mut next.retained_assets { + if matches!(key.source, SurfaceGraphicsSource::Terminal { .. }) + && !self.assets.contains_key(key) + && self.host.images.get(&self.image_id(key)) + != Some(&image_signature_from_asset(key)) + { + if let Some(previous) = previous_assets.get(&key.source) { + *key = (*previous).clone(); + } + } + } + let desired = next + .placements + .iter() + .map(|p| &p.asset) + .chain(next.retained_assets.iter()) + .collect::<HashSet<_>>(); + let stale = self + .display_scene + .placements + .iter() + .map(|p| &p.asset) + .chain(self.display_scene.retained_assets.iter()) + .filter(|key| !desired.contains(key)) + .map(|key| self.image_id(key)) + .collect::<Vec<_>>(); + self.stale_images.extend(stale); + let owned = self + .scene + .placements + .iter() + .map(|p| &p.asset) + .chain(self.scene.retained_assets.iter()) + .chain(desired.iter().copied()) + .collect::<HashSet<_>>(); + self.assets.retain(|key, _| owned.contains(key)); + self.native_image_ids.retain(|key, _| owned.contains(key)); + self.display_scene = next; + } + + #[cfg(test)] pub(crate) fn encode( &mut self, visibility: Visibility, @@ -223,6 +369,18 @@ impl ClientState { cell_size: HostCellSize, occlusion: &Occlusion, ) -> Vec<u8> { + self.encode_output(visibility, main_origin, popup_origin, cell_size, occlusion) + .into_inline_bytes() + } + + pub(crate) fn encode_output( + &mut self, + visibility: Visibility, + main_origin: (u16, u16), + popup_origin: Option<(u16, u16)>, + cell_size: HostCellSize, + occlusion: &Occlusion, + ) -> GraphicsOutput { let mut bytes = self.take_pending_cleanup(); self.stale_images.sort_unstable(); self.stale_images.dedup(); @@ -238,69 +396,52 @@ impl ClientState { } if !cell_size.is_known() || self.scope.is_empty() { bytes.extend(self.host.clear_bytes()); - return bytes; + return GraphicsOutput::from_bytes(bytes); } let placements = self - .scene + .display_scene .placements .iter() .filter_map(|placement| { client_host_placement( &self.scope, placement, - self.assets.get(&placement.asset).map(Vec::as_slice), visibility, main_origin, popup_origin, cell_size, occlusion, ) + .map(|mut host| { + host.host_image_id = Some(self.image_id(&placement.asset)); + host.raw_data = self.assets.get(&placement.asset).map(Arc::clone); + host + }) }) .collect::<Vec<_>>(); + let mut output = GraphicsOutput::from_bytes(bytes); self.host.request_placement_replay(); loop { - let encoded = encode_graphics_update_incremental( - &mut self.host, - &placements, - &HashSet::new(), - None, - false, - ); - bytes.extend(encoded.bytes); - if !encoded.incomplete { - return bytes; + let (encoded, incomplete) = + encode_graphics_output_incremental(&mut self.host, &placements); + output.extend(encoded); + if !incomplete { + return output; } } } } -pub(crate) fn pane_layer_asset_key( - app: &crate::app::App, - key: &crate::app::pane_graphics::Key, - layer: &crate::app::pane_graphics::Layer, -) -> Option<SurfaceGraphicsAssetKey> { - let workspace_index = app - .state - .workspaces - .iter() - .position(|workspace| workspace.pane_state(key.0).is_some())?; - Some(SurfaceGraphicsAssetKey { - source: SurfaceGraphicsSource::PaneLayer { - pane_id: app.public_pane_id(workspace_index, key.0)?, - layer_id: key.1.clone(), - }, - image_width: layer.image_width, - image_height: layer.image_height, - format: match layer.format { - crate::api::schema::PaneGraphicsFormat::Rgb => SurfaceGraphicsFormat::Rgb, - crate::api::schema::PaneGraphicsFormat::Rgba - | crate::api::schema::PaneGraphicsFormat::Bgra => SurfaceGraphicsFormat::Rgba, - crate::api::schema::PaneGraphicsFormat::Png => SurfaceGraphicsFormat::Png, - }, - data_len: layer.data_len() as u64, - data_fingerprint: layer.data_fingerprint, - }) +pub(crate) const NATIVE_TRANSFER_BIT: u64 = 1 << 63; +pub(crate) const NATIVE_SLOT_BIT: u32 = 1 << 29; + +pub(crate) fn native_host_image_id(scope: &str, key: &SurfaceGraphicsAssetKey) -> u32 { + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + scope.hash(&mut hasher); + key.source.hash(&mut hasher); + // Keep native IDs separate from the direct API namespace. + 0x4000_0000 | (hasher.finish() as u32 & 0x1fff_ffff) } pub(crate) fn host_image_id(scope: &str, key: &SurfaceGraphicsAssetKey) -> u32 { @@ -310,6 +451,7 @@ pub(crate) fn host_image_id(scope: &str, key: &SurfaceGraphicsAssetKey) -> u32 { 10_000 + ((hasher.finish() as u32) % 900_000) } +#[cfg(all(test, unix))] pub(crate) fn direct_upload_control(scope: &str, key: &SurfaceGraphicsAssetKey) -> (u32, String) { let image_id = host_image_id(scope, key); ( @@ -329,10 +471,14 @@ pub(crate) fn collect_scene( popup_content_size: Option<(u16, u16)>, cell_size: HostCellSize, delivered: &DeliveryCache, - client_id: u64, -) -> (SurfaceGraphicsScene, DeliveryCache) { + _client_id: u64, +) -> (SurfaceGraphicsScene, DeliveryCache, SourceFiles) { if !cell_size.is_known() { - return (SurfaceGraphicsScene::default(), DeliveryCache::default()); + return ( + SurfaceGraphicsScene::default(), + DeliveryCache::default(), + SourceFiles::default(), + ); } let workspace_index = surface.target.map(|target| target.workspace_index); let mut targets = HashMap::new(); @@ -357,10 +503,9 @@ pub(crate) fn collect_scene( targets.insert(*pane_id, target.clone()); } - // Reconstruct only the small image-signature index expected by the existing - // collector. This prevents copying already-delivered image payloads on each - // pane-scaled render while keeping Ghostty as the authoritative image store. - let mut uploaded_images = HashMap::new(); + // Reconstruct the small source-signature index expected by the collector. + // This avoids copying already-delivered payloads while Ghostty remains the + // authoritative image store. let mut delivered_terminal_images = HashMap::new(); for key in &delivered.assets { let signature = image_signature_from_asset(key); @@ -379,13 +524,8 @@ pub(crate) fn collect_scene( ); } } - SurfaceGraphicsSource::PaneLayer { pane_id, layer_id } => { - if let Some(pane_id) = public_panes.get(pane_id) { - if let Some(slot) = app.pane_graphics.slots.get(&(*pane_id, layer_id.clone())) { - uploaded_images.insert(slot.host_image_id, signature); - } - } - } + // Frozen for old-server compatibility; current servers no longer collect layers. + SurfaceGraphicsSource::PaneLayer { .. } => {} SurfaceGraphicsSource::Terminal { target: SurfaceGraphicsTarget::Popup { .. }, .. @@ -394,13 +534,10 @@ pub(crate) fn collect_scene( } let mut host_placements = collect_visible_placements( &app.state, - &app.pane_graphics, &app.terminal_runtimes, surface, cell_size, - &uploaded_images, &delivered_terminal_images, - client_id, ); if let (Some(popup), Some((width, height)), Some((_, target))) = ( @@ -418,9 +555,10 @@ pub(crate) fn collect_scene( }, descriptor, ); - !delivered.assets.contains(&key) && requested.insert(key) + !descriptor.source_file && !delivered.assets.contains(&key) && requested.insert(key) }) { host_placements.push(HostPlacement { + raw_data: None, pane_id: popup.pane_id, host_image_id: None, area: Rect::new(0, 0, width, height), @@ -441,6 +579,7 @@ pub(crate) fn collect_scene( let mut placements = Vec::new(); let mut asset_data = HashMap::<SurfaceGraphicsAssetKey, Vec<u8>>::new(); + let mut sources = SourceFiles::default(); for mut placement in host_placements { if placements.len() == MAX_SURFACE_GRAPHICS_PLACEMENTS { break; @@ -453,21 +592,18 @@ pub(crate) fn collect_scene( target, image_id: *image_id, }, - HostSourceKey::PaneLayer { layer_id, .. } => { - let SurfaceGraphicsTarget::Pane { pane_id } = target else { - continue; - }; - SurfaceGraphicsSource::PaneLayer { - pane_id, - layer_id: layer_id.clone(), - } - } HostSourceKey::ClientSurface { .. } => continue, }; let Some((clipped, _)) = clipped_placement(&placement) else { continue; }; let asset = asset_key(source, &placement.placement); + if !delivered.assets.contains(&asset) { + if let Some(file) = placement.placement.source_file.take() { + sources.entry(asset.clone()).or_insert(file); + asset_data.entry(asset.clone()).or_default(); + } + } if !placement.placement.data.is_empty() { asset_data .entry(asset.clone()) @@ -507,7 +643,9 @@ pub(crate) fn collect_scene( if next.assets.contains(&key) { continue; } - let encoded_size = super::image_transfer_estimated_size(data.len()); + let encoded_size = super::image_transfer_estimated_size( + usize::try_from(key.data_len).unwrap_or(usize::MAX), + ); if encoded_size > super::HEADLESS_GRAPHICS_TRANSACTION_BUDGET { continue; } @@ -532,17 +670,8 @@ pub(crate) fn collect_scene( placement.x, ) }); - let mut retained_assets = app - .pane_graphics - .slots - .iter() - .filter_map(|(key, slot)| { - let layer = slot.layer.as_ref()?; - (slot.direct_client() == Some(client_id)) - .then(|| pane_layer_asset_key(app, key, layer))? - }) - .collect::<Vec<_>>(); - retained_assets.sort_by_key(|key| format!("{:?}", key.source)); + let retained_assets = Vec::new(); + sources.retain(|key, _| next.assets.contains(key)); ( SurfaceGraphicsScene { assets, @@ -550,6 +679,7 @@ pub(crate) fn collect_scene( retained_assets, }, next, + sources, ) } @@ -602,7 +732,6 @@ fn asset_key( fn client_host_placement( scope: &str, placement: &SurfaceGraphicsPlacement, - data: Option<&[u8]>, visibility: Visibility, main_origin: (u16, u16), popup_origin: Option<(u16, u16)>, @@ -653,6 +782,7 @@ fn client_host_placement( let cols = placement.cols.min(u32::from(u16::MAX)) as u16; let rows = placement.rows.min(u32::from(u16::MAX)) as u16; Some(HostPlacement { + raw_data: None, pane_id, host_image_id: Some(host_image_id), area: Rect::new( @@ -678,7 +808,8 @@ fn client_host_placement( }, data_len: usize::try_from(placement.asset.data_len).unwrap_or(usize::MAX), data_fingerprint: placement.asset.data_fingerprint, - data: data.unwrap_or_default().to_vec(), + source_file: None, + data: Vec::new(), render: KittyPlacementRenderInfo { pixel_width: placement.cols.saturating_mul(cell_size.width_px), pixel_height: placement.rows.saturating_mul(cell_size.height_px), @@ -753,6 +884,387 @@ mod tests { } #[test] + fn legacy_pane_layer_scene_still_uploads_and_places() { + let mut state = ClientState::default(); + state.set_scope("old-server-pane-layer"); + let mut image = asset( + SurfaceGraphicsTarget::Pane { + pane_id: "pane".into(), + }, + 1, + vec![1, 2, 3, 255], + ); + image.key.source = SurfaceGraphicsSource::PaneLayer { + pane_id: "pane".into(), + layer_id: "primary".into(), + }; + state.set_scene(scene(image, 2, 1)); + + let bytes = String::from_utf8(state.encode( + Visibility::Main, + (10, 5), + None, + HostCellSize { + width_px: 8, + height_px: 16, + }, + &Occlusion::default(), + )) + .unwrap(); + + assert!(bytes.contains("a=t,t=d"), "{bytes:?}"); + assert!(bytes.contains("a=p,"), "{bytes:?}"); + assert!(bytes.contains("\u{1b}[7;13H"), "{bytes:?}"); + } + + #[test] + fn stable_ids_replace_native_pixels_and_delete_on_removal() { + use crate::kitty_graphics::GraphicsOperation; + let mut state = ClientState::default(); + state.set_scope("stable-native"); + let target = SurfaceGraphicsTarget::Pane { + pane_id: "pane".into(), + }; + let first = asset(target.clone(), 1, vec![1, 2, 3, 255]); + let id = state.image_id(&first.key); + let cell = HostCellSize { + width_px: 8, + height_px: 16, + }; + for revision in 1..=8 { + let next = asset(target.clone(), revision, vec![revision as u8, 2, 3, 255]); + assert_eq!(state.image_id(&next.key), id); + state.set_scene(scene(next, 0, 0)); + let output = + state.encode_output(Visibility::Main, (0, 0), None, cell, &Occlusion::default()); + let uploads = output + .operations + .iter() + .filter_map(|op| match op { + GraphicsOperation::Upload { control, data } => Some((control, data)), + _ => None, + }) + .collect::<Vec<_>>(); + assert_eq!(uploads.len(), 1); + assert!(uploads[0].0.contains(&format!("i={id},"))); + assert_eq!(uploads[0].1.as_ref(), &[revision as u8, 2, 3, 255]); + let bytes = String::from_utf8(output.into_inline_bytes()).unwrap(); + let upload = bytes.find("a=t,").unwrap(); + let display = bytes.find("a=p,").unwrap(); + assert!(upload < display); + if revision > 1 { + assert!(bytes.find("a=d,d=I,").unwrap() < upload); + } + assert_eq!(state.host.images.len(), 1); + } + let mut other = first.key.clone(); + other.source = SurfaceGraphicsSource::Terminal { + target: SurfaceGraphicsTarget::Pane { + pane_id: "other-pane".into(), + }, + image_id: 7, + }; + assert_ne!(state.image_id(&other), id); + other.source = SurfaceGraphicsSource::PaneLayer { + pane_id: "pane".into(), + layer_id: "layer".into(), + }; + assert_eq!(state.image_id(&other), host_image_id(state.scope(), &other)); + let legacy_id = state.image_id(&other); + other.data_fingerprint += 1; + assert_ne!(state.image_id(&other), legacy_id); + state.set_scene(SurfaceGraphicsScene::default()); + let bytes = state.encode(Visibility::Main, (0, 0), None, cell, &Occlusion::default()); + assert!(String::from_utf8(bytes) + .unwrap() + .contains(&format!("a=d,d=I,i={id}"))); + assert!(state.host.images.is_empty()); + } + + #[cfg(unix)] + #[test] + fn native_file_adoption_keeps_previous_frame_until_replacement_ack() { + let mut state = ClientState::default(); + state.set_scope("native-files"); + let image = asset( + SurfaceGraphicsTarget::Pane { + pane_id: "p1".into(), + }, + 1, + vec![1, 2, 3, 255], + ); + let id = native_host_image_id(state.scope(), &image.key); + assert!(!state.accepts_direct_asset(&image.key, id)); + let mut metadata = scene(image.clone(), 0, 0); + metadata.assets.clear(); + state.set_scene(metadata); + let cell = HostCellSize { + width_px: 8, + height_px: 16, + }; + state.encode(Visibility::Main, (0, 0), None, cell, &Occlusion::default()); + assert!(state.trust_direct_asset(&image.key, id)); + let first = String::from_utf8(state.encode( + Visibility::Main, + (0, 0), + None, + cell, + &Occlusion::default(), + )) + .unwrap(); + assert!(first.contains("a=p,")); + assert!(!first.contains("a=t,")); + let mut next = image.clone(); + next.key.data_fingerprint += 1; + let mut metadata = scene(next.clone(), 0, 0); + metadata.assets.clear(); + state.set_scene(metadata); + assert!(!state.trust_direct_asset(&image.key, id)); + let cleanup = String::from_utf8(state.encode( + Visibility::Main, + (0, 0), + None, + cell, + &Occlusion::default(), + )) + .unwrap(); + assert!(!cleanup.contains("a=d,")); + assert!(cleanup.contains(&format!("a=p,i={id},"))); + assert!(!state.accepts_direct_asset(&next.key, id)); + let replacement_id = id ^ NATIVE_SLOT_BIT; + assert!(state.trust_direct_asset(&next.key, replacement_id)); + let placed = String::from_utf8(state.encode( + Visibility::Main, + (0, 0), + None, + cell, + &Occlusion::default(), + )) + .unwrap(); + assert!(placed.contains(&format!("a=p,i={replacement_id},"))); + assert!(placed.contains(&format!("a=d,d=I,i={id},"))); + assert!(!placed.contains("a=t,")); + assert_eq!(state.host.images.len(), 1); + assert_eq!(state.native_image_ids.len(), 1); + // The vacated bank is reusable, but the visible bank cannot be overwritten. + assert!(state.accepts_direct_asset(&next.key, id)); + assert!(!state.accepts_direct_asset(&next.key, replacement_id)); + assert!(!state.accepts_direct_asset(&next.key, host_image_id(state.scope(), &next.key))); + } + + #[cfg(unix)] + #[test] + fn native_replacements_use_two_ids_without_blank_pending_frames() { + let mut state = ClientState::default(); + state.set_scope("double-buffer"); + let cell = HostCellSize { + width_px: 8, + height_px: 16, + }; + let mut previous = None; + let mut ids = HashSet::new(); + for revision in 0..32 { + let image = asset( + SurfaceGraphicsTarget::Pane { + pane_id: "pane".into(), + }, + revision, + vec![revision as u8, 2, 3, 255], + ); + let base = native_host_image_id(state.scope(), &image.key); + let id = if revision % 2 == 0 { + base ^ NATIVE_SLOT_BIT + } else { + base + }; + ids.insert(id); + let mut metadata = scene(image.clone(), 0, 0); + metadata.assets.clear(); + state.set_scene(metadata.clone()); + for _ in 0..3 { + state.set_scene(metadata.clone()); + let pending = String::from_utf8(state.encode( + Visibility::Main, + (0, 0), + None, + cell, + &Occlusion::default(), + )) + .unwrap(); + assert!(!pending.contains("a=d,"), "{pending:?}"); + if let Some(previous) = previous { + assert!(pending.contains(&format!("a=p,i={previous},"))); + assert!(!state.accepts_direct_asset(&image.key, previous)); + } + } + assert!(state.trust_direct_asset(&image.key, id)); + let swapped = String::from_utf8(state.encode( + Visibility::Main, + (0, 0), + None, + cell, + &Occlusion::default(), + )) + .unwrap(); + assert!(swapped.contains(&format!("a=p,i={id},"))); + assert!(!swapped.contains(&format!("a=d,d=I,i={id},"))); + if let Some(previous) = previous { + assert!(swapped.contains(&format!("a=d,d=I,i={previous},"))); + } + assert_eq!(state.host.images.len(), 1); + assert_eq!(state.native_image_ids.len(), 1); + assert!(!state.accepts_direct_asset(&image.key, id)); + assert_eq!(id & 0x8000_0000, 0, "native IDs must not alias API IDs"); + previous = Some(id); + } + assert_eq!(ids.len(), 2); + state.set_scene(SurfaceGraphicsScene::default()); + let cleanup = String::from_utf8(state.encode( + Visibility::Main, + (0, 0), + None, + cell, + &Occlusion::default(), + )) + .unwrap(); + assert!(cleanup.contains(&format!("a=d,d=I,i={},", previous.unwrap()))); + assert!(state.host.images.is_empty()); + assert!(state.native_image_ids.is_empty()); + } + + #[cfg(unix)] + #[test] + fn native_pending_replacement_failure_and_geometry_do_not_preserve_stale_frames() { + let mut state = ClientState::default(); + state.set_scope("replacement-fallback"); + let cell = HostCellSize { + width_px: 8, + height_px: 16, + }; + let image = asset( + SurfaceGraphicsTarget::Pane { + pane_id: "pane".into(), + }, + 1, + vec![1, 2, 3, 255], + ); + let base = native_host_image_id(state.scope(), &image.key); + let active = base ^ NATIVE_SLOT_BIT; + let mut metadata = scene(image.clone(), 0, 0); + metadata.assets.clear(); + state.set_scene(metadata); + state.encode(Visibility::Main, (0, 0), None, cell, &Occlusion::default()); + assert!(state.trust_direct_asset(&image.key, active)); + state.encode(Visibility::Main, (0, 0), None, cell, &Occlusion::default()); + let mut next = image.clone(); + next.key.data_fingerprint += 1; + let mut metadata = scene(next.clone(), 0, 0); + metadata.assets.clear(); + state.set_scene(metadata); + state.retire_direct_image(base); + let rejected = String::from_utf8(state.encode( + Visibility::Main, + (0, 0), + None, + cell, + &Occlusion::default(), + )) + .unwrap(); + assert!(!rejected.contains(&format!("a=d,d=I,i={active},"))); + assert!(rejected.contains(&format!("a=p,i={active},"))); + state.set_scene(scene(next.clone(), 0, 0)); + let fallback = String::from_utf8(state.encode( + Visibility::Main, + (0, 0), + None, + cell, + &Occlusion::default(), + )) + .unwrap(); + assert!(fallback.contains("a=t,")); + assert!(fallback.contains(&format!("a=p,i={base},"))); + assert!(fallback.contains(&format!("a=d,d=I,i={active},"))); + assert!(!state.accepts_direct_asset(&next.key, base)); + next.key.data_fingerprint += 1; + assert!(!state.accepts_direct_asset(&next.key, base)); + let mut moved = scene(next.clone(), 1, 0); + moved.assets.clear(); + state.set_scene(moved); + let geometry = String::from_utf8(state.encode( + Visibility::Main, + (0, 0), + None, + cell, + &Occlusion::default(), + )) + .unwrap(); + assert!(geometry.contains(&format!("a=d,d=I,i={base},"))); + assert!(!geometry.contains("a=p,")); + state.set_scene(SurfaceGraphicsScene::default()); + assert!(!state.trust_direct_asset(&next.key, active)); + } + + #[test] + fn deferred_client_reuses_resident_arc_and_orders_cleanup_before_upload() { + use super::super::output::GraphicsOperation; + let mut state = ClientState::default(); + state.set_scope("raw"); + let image = asset( + SurfaceGraphicsTarget::Pane { + pane_id: "pane".into(), + }, + 1, + vec![1, 2, 3, 4], + ); + let key = image.key.clone(); + let mut graphics = scene(image, 0, 0); + let mut second = graphics.placements[0].clone(); + second.logical_placement_id += 1; + second.x = 2; + graphics.placements.push(second); + let mut replacement = graphics.clone(); + replacement.assets.clear(); + state.set_scene(graphics); + let resident = Arc::clone(&state.assets[&key]); + state.set_scene(replacement); + assert!(Arc::ptr_eq(&resident, &state.assets[&key])); + state.forced_delete_images.push(4242); + let cell = HostCellSize { + width_px: 8, + height_px: 16, + }; + let output = + state.encode_output(Visibility::Main, (0, 0), None, cell, &Occlusion::default()); + assert!( + matches!(&output.operations[0], GraphicsOperation::Bytes(bytes) + if String::from_utf8_lossy(bytes).contains("a=d,d=I,i=4242")) + ); + 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], &resident)); + assert_eq!( + String::from_utf8(output.clone().into_inline_bytes()) + .unwrap() + .matches("a=p") + .count(), + 2 + ); + let replay = + state.encode_output(Visibility::Main, (0, 0), None, cell, &Occlusion::default()); + assert!(replay + .operations + .iter() + .all(|op| matches!(op, GraphicsOperation::Bytes(_)))); + } + + #[test] fn occlusion_uses_final_pixel_bounds_and_strict_overlap() { let cell = HostCellSize { width_px: 8, @@ -797,7 +1309,7 @@ mod tests { 1, vec![1, 2, 3, 4], ); - let id = host_image_id("occlusion", &image.key); + let id = state.image_id(&image.key); let mut graphics = scene(image.clone(), 0, 0); let mut second = graphics.placements[0].clone(); second.logical_placement_id = 4; @@ -907,12 +1419,12 @@ mod tests { cover.cover(Rect::new(0, 10, 80, 3)); cover.cover(Rect::new(60, 0, 20, 5)); } - let _ = state.encode(Visibility::Main, (0, 0), None, cell, &cover); + let _ = state.encode_output(Visibility::Main, (0, 0), None, cell, &cover); let mut samples = Vec::new(); for _ in 0..101 { let start = std::time::Instant::now(); for _ in 0..20 { - std::hint::black_box(state.encode( + std::hint::black_box(state.encode_output( Visibility::Main, (0, 0), None, @@ -923,7 +1435,57 @@ mod tests { samples.push(start.elapsed().as_nanos() / 20); } samples.sort_unstable(); - eprintln!("graphics occlusion panes={count} disjoint_overlays={covered} median_ns={} p95_ns={}", samples[50], samples[95]); + eprintln!( + "graphics occlusion panes={count} disjoint_overlays={covered} median_ns={} p95_ns={}", + samples[50], samples[95] + ); + } + #[cfg(unix)] + { + let mut state = ClientState::default(); + state.set_scope("swap-profile"); + state.set_scene(graphics.clone()); + state.encode_output(Visibility::Main, (0, 0), None, cell, &Occlusion::default()); + graphics.assets.clear(); + let mut samples = Vec::new(); + for revision in 1..=105 { + graphics.placements[0].asset.data_fingerprint = revision; + let key = graphics.placements[0].asset.clone(); + let base = native_host_image_id(state.scope(), &key); + let id = if revision % 2 == 1 { + base ^ NATIVE_SLOT_BIT + } else { + base + }; + let scene = graphics.clone(); + let start = std::time::Instant::now(); + state.set_scene(scene); + std::hint::black_box(state.encode_output( + Visibility::Main, + (0, 0), + None, + cell, + &Occlusion::default(), + )); + let checkpoint = state.clone(); + assert!(state.trust_direct_asset(&key, id)); + std::hint::black_box(state.encode_output( + Visibility::Main, + (0, 0), + None, + cell, + &Occlusion::default(), + )); + drop(checkpoint); + if revision > 5 { + samples.push(start.elapsed().as_nanos()); + } + } + samples.sort_unstable(); + eprintln!( + "graphics replacement panes={count} median_ns={} p95_ns={}", + samples[50], samples[95] + ); } } } @@ -1031,7 +1593,7 @@ mod tests { #[cfg(unix)] #[test] - fn trusted_direct_asset_is_placed_without_inline_reupload() { + fn legacy_direct_asset_is_placed_without_inline_reupload() { let mut state = ClientState::default(); state.set_scope("endpoint-a:boot-1"); let _ = state.encode( @@ -1044,14 +1606,18 @@ mod tests { }, &Occlusion::default(), ); - let image = asset( + let mut image = asset( SurfaceGraphicsTarget::Pane { pane_id: "w1:p1".into(), }, 15, vec![1, 2, 3, 4], ); - let image_id = host_image_id("endpoint-a:boot-1", &image.key); + image.key.source = SurfaceGraphicsSource::PaneLayer { + pane_id: "w1:p1".into(), + layer_id: "primary".into(), + }; + let image_id = state.image_id(&image.key); assert!(state.trust_direct_asset(&image.key, image_id)); let mut direct_scene = scene(image, 0, 0); direct_scene.assets.clear(); @@ -1085,7 +1651,7 @@ mod tests { 18, vec![1, 2, 3, 4], ); - let image_id = host_image_id("endpoint-a:boot-1", &image.key); + let image_id = state.image_id(&image.key); let mut direct_scene = scene(image.clone(), 0, 0); direct_scene.assets.clear(); state.set_scene(direct_scene); @@ -1119,7 +1685,7 @@ mod tests { 19, vec![1, 2, 3, 4], ); - let image_id = host_image_id("endpoint-a:boot-1", &image.key); + let image_id = state.image_id(&image.key); let mut active = scene(image.clone(), 0, 0); active.assets.clear(); state.set_scene(active.clone()); @@ -1235,18 +1801,22 @@ mod tests { #[cfg(unix)] #[test] - fn unclaimed_direct_asset_is_deleted_by_the_next_authoritative_scene() { + fn unclaimed_legacy_direct_asset_is_deleted_by_the_next_authoritative_scene() { let mut state = ClientState::default(); state.set_scope("endpoint-a:boot-1"); let _ = state.take_pending_cleanup(); - let image = asset( + let mut image = asset( SurfaceGraphicsTarget::Pane { pane_id: "w1:p1".into(), }, 16, vec![1, 2, 3, 4], ); - let image_id = host_image_id("endpoint-a:boot-1", &image.key); + image.key.source = SurfaceGraphicsSource::PaneLayer { + pane_id: "w1:p1".into(), + layer_id: "primary".into(), + }; + let image_id = state.image_id(&image.key); assert!(state.trust_direct_asset(&image.key, image_id)); state.set_scene(SurfaceGraphicsScene::default()); |
