aboutsummaryrefslogtreecommitdiff
path: root/src/kitty_graphics/surface.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/kitty_graphics/surface.rs')
-rw-r--r--src/kitty_graphics/surface.rs790
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());