use crate::ansi::sixel;
use crate::config::colors::ColorRgb;
use crate::crosswords::grid::Dimensions;
use crate::sugarloaf::{GraphicData, GraphicId};
use parking_lot::Mutex;
use rustc_hash::FxHashMap;
use smallvec::SmallVec;
use std::mem;
use std::sync::{Arc, Weak};
use tracing::debug;
#[derive(Debug, Clone)]
pub struct UpdateQueues {
pub pending: Vec<GraphicData>,
pub pending_images: Vec<(u32, GraphicData)>,
pub remove_queue: Vec<GraphicId>,
}
#[derive(Clone, Debug)]
pub struct TextureRef {
pub id: GraphicId,
pub width: u16,
pub height: u16,
pub cell_height: usize,
pub texture_operations: Weak<Mutex<Vec<GraphicId>>>,
}
impl PartialEq for TextureRef {
fn eq(&self, t: &Self) -> bool {
self.id == t.id
}
}
impl Eq for TextureRef {}
impl Drop for TextureRef {
fn drop(&mut self) {
if let Some(texture_operations) = self.texture_operations.upgrade() {
texture_operations.lock().push(self.id);
}
}
}
pub type GraphicsCell = SmallVec<[GraphicCell; 1]>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GraphicCell {
pub texture: Arc<TextureRef>,
pub offset_x: u16,
pub offset_y: u16,
}
pub const KITTY_PLACEHOLDER: char = '\u{10EEEE}';
#[derive(Debug, Clone, PartialEq)]
pub struct StoredImage {
pub data: GraphicData,
pub transmission_time: std::time::Instant,
}
#[derive(Debug, Clone, PartialEq)]
pub struct KittyPlacement {
pub image_id: u32,
pub placement_id: u32,
pub source_x: u32,
pub source_y: u32,
pub source_width: u32,
pub source_height: u32,
pub dest_col: usize,
pub dest_row: i64,
pub columns: u32,
pub rows: u32,
pub pixel_width: u32,
pub pixel_height: u32,
pub cell_x_offset: u32,
pub cell_y_offset: u32,
pub z_index: i32,
pub transmit_time: std::time::Instant,
}
#[derive(Debug, Clone, PartialEq)]
pub struct VirtualPlacement {
pub image_id: u32,
pub placement_id: u32,
pub columns: u32,
pub rows: u32,
pub x: u32,
pub y: u32,
}
#[derive(Debug)]
pub struct Graphics {
pub last_id: u64,
pub pending: Vec<GraphicData>,
pub pending_images: Vec<(u32, GraphicData)>,
pub texture_operations: Arc<Mutex<Vec<GraphicId>>>,
pub sixel_shared_palette: Option<Vec<ColorRgb>>,
pub cell_height: f32,
pub cell_width: f32,
pub sixel_parser: Option<Box<sixel::Parser>>,
pub kitty_images: FxHashMap<u32, StoredImage>,
pub kitty_image_numbers: FxHashMap<u32, u32>,
pub kitty_virtual_placements: FxHashMap<(u32, u32), VirtualPlacement>,
pub kitty_chunking_state: crate::ansi::kitty_graphics_protocol::KittyGraphicsState,
pub total_bytes: usize,
pub total_limit: usize,
pub image_timestamps: FxHashMap<GraphicId, std::time::Instant>,
pub placed_textures: FxHashMap<GraphicId, Weak<TextureRef>>,
pub kitty_placements: FxHashMap<(u32, u32), KittyPlacement>,
pub kitty_graphics_dirty: bool,
}
impl Default for Graphics {
fn default() -> Self {
Self {
last_id: 0,
pending: Vec::new(),
pending_images: Vec::new(),
texture_operations: Arc::new(Mutex::new(Vec::new())),
sixel_shared_palette: None,
cell_height: 0.0,
cell_width: 0.0,
sixel_parser: None,
kitty_images: FxHashMap::default(),
kitty_image_numbers: FxHashMap::default(),
kitty_virtual_placements: FxHashMap::default(),
kitty_chunking_state:
crate::ansi::kitty_graphics_protocol::KittyGraphicsState::default(),
total_bytes: 0,
total_limit: 320 * 1024 * 1024, image_timestamps: FxHashMap::default(),
placed_textures: FxHashMap::default(),
kitty_placements: FxHashMap::default(),
kitty_graphics_dirty: false,
}
}
}
impl Graphics {
pub fn new<S: Dimensions>(size: &S) -> Self {
let mut graphics = Graphics::default();
graphics.resize(size);
graphics
}
pub fn next_id(&mut self) -> GraphicId {
self.last_id += 1;
GraphicId::new(self.last_id)
}
pub fn has_pending_updates(&self) -> bool {
!self.pending.is_empty()
|| !self.pending_images.is_empty()
|| !self.texture_operations.lock().is_empty()
}
pub fn take_queues(&mut self) -> Option<UpdateQueues> {
let remove_queue = {
let mut queue = self.texture_operations.lock();
if queue.is_empty() {
Vec::new()
} else {
mem::take(&mut *queue)
}
};
if remove_queue.is_empty()
&& self.pending.is_empty()
&& self.pending_images.is_empty()
{
return None;
}
Some(UpdateQueues {
pending: mem::take(&mut self.pending),
pending_images: mem::take(&mut self.pending_images),
remove_queue,
})
}
pub fn resize<S: Dimensions>(&mut self, size: &S) {
self.cell_height = size.square_height();
self.cell_width = size.square_width();
}
pub fn store_kitty_image(
&mut self,
image_id: u32,
image_number: Option<u32>,
mut data: GraphicData,
) {
let now = std::time::Instant::now();
data.transmit_time = now;
let new_bytes = data.pixels.len();
if self.total_bytes + new_bytes > self.total_limit {
let mut active = std::collections::HashSet::new();
for placement in self.kitty_placements.values() {
active.insert(placement.image_id as u64);
}
active.insert(image_id as u64);
self.evict_images(new_bytes, &active);
}
if let Some(old) = self.kitty_images.get(&image_id) {
self.total_bytes = self.total_bytes.saturating_sub(old.data.pixels.len());
}
self.kitty_images.insert(
image_id,
StoredImage {
data,
transmission_time: now,
},
);
self.total_bytes += new_bytes;
if let Some(number) = image_number {
self.kitty_image_numbers.insert(number, image_id);
}
}
pub fn get_kitty_image(&self, image_id: u32) -> Option<&StoredImage> {
self.kitty_images.get(&image_id)
}
pub fn get_kitty_image_by_number(&self, image_number: u32) -> Option<&StoredImage> {
self.kitty_image_numbers
.get(&image_number)
.and_then(|id| self.kitty_images.get(id))
}
pub fn delete_kitty_images(
&mut self,
predicate: impl Fn(&u32, &StoredImage) -> bool,
) {
self.kitty_images.retain(|id, img| !predicate(id, img));
self.kitty_image_numbers
.retain(|_, id| self.kitty_images.contains_key(id));
}
fn calculate_graphic_bytes(graphic: &GraphicData) -> usize {
graphic.pixels.len()
}
pub fn evict_images(
&mut self,
required_bytes: usize,
used_ids: &std::collections::HashSet<u64>,
) -> bool {
use tracing::debug;
if self.total_bytes + required_bytes <= self.total_limit {
return true; }
let bytes_to_free = (self.total_bytes + required_bytes) - self.total_limit;
debug!("Graphics memory: need to evict {} bytes (current: {}, limit: {}, required: {})",
bytes_to_free, self.total_bytes, self.total_limit, required_bytes);
let mut candidates: Vec<(GraphicId, std::time::Instant, bool, usize)> =
Vec::new();
for graphic in &self.pending {
if let Some(×tamp) = self.image_timestamps.get(&graphic.id) {
let is_used = used_ids.contains(&graphic.id.get());
let bytes = Self::calculate_graphic_bytes(graphic);
candidates.push((graphic.id, timestamp, is_used, bytes));
}
}
for (&kitty_id, stored) in &self.kitty_images {
let id_as_u64 = kitty_id as u64;
let is_used = used_ids.contains(&id_as_u64);
let bytes = Self::calculate_graphic_bytes(&stored.data);
candidates.push((
GraphicId::new(id_as_u64),
stored.transmission_time,
is_used,
bytes,
));
}
if candidates.is_empty() {
debug!("No candidates for eviction");
return false;
}
candidates.sort_by(|a, b| {
match (a.2, b.2) {
(false, true) => std::cmp::Ordering::Less, (true, false) => std::cmp::Ordering::Greater, _ => a.1.cmp(&b.1), }
});
let mut freed_bytes = 0usize;
let mut evicted_ids = Vec::new();
for (graphic_id, _, is_used, bytes) in candidates {
if freed_bytes >= bytes_to_free {
break;
}
evicted_ids.push(graphic_id);
freed_bytes += bytes;
debug!(
"Evicting graphic id={}, bytes={}, used={}",
graphic_id.get(),
bytes,
is_used
);
}
for id in evicted_ids {
self.pending.retain(|g| g.id != id);
let evicted_u32 = id.get() as u32;
self.kitty_images.remove(&evicted_u32);
self.kitty_placements
.retain(|_, p| p.image_id != evicted_u32);
self.image_timestamps.remove(&id);
self.texture_operations.lock().push(id);
}
self.total_bytes = self.total_bytes.saturating_sub(freed_bytes);
debug!(
"Evicted {} bytes, new total: {}",
freed_bytes, self.total_bytes
);
freed_bytes >= bytes_to_free
}
pub fn register_placed_texture(
&mut self,
graphic_id: GraphicId,
weak: Weak<TextureRef>,
) {
self.placed_textures.insert(graphic_id, weak);
}
pub fn collect_active_graphic_ids(&mut self) -> std::collections::HashSet<u64> {
let mut active = std::collections::HashSet::new();
self.placed_textures.retain(|id, weak| {
if weak.strong_count() > 0 {
active.insert(id.get());
true
} else {
false
}
});
for placement in self.kitty_placements.values() {
active.insert(placement.image_id as u64);
}
active
}
pub fn track_graphic(&mut self, graphic_id: GraphicId, bytes: usize) {
self.image_timestamps
.insert(graphic_id, std::time::Instant::now());
self.total_bytes += bytes;
debug!(
"Tracked graphic id={}, bytes={}, total_bytes={}",
graphic_id.0, bytes, self.total_bytes
);
}
pub fn untrack_graphic(&mut self, graphic_id: GraphicId, bytes: usize) {
self.image_timestamps.remove(&graphic_id);
self.total_bytes = self.total_bytes.saturating_sub(bytes);
debug!(
"Untracked graphic id={}, bytes={}, total_bytes={}",
graphic_id.0, bytes, self.total_bytes
);
}
}
#[test]
fn check_opaque_region() {
use sugarloaf::ColorType;
let graphic = GraphicData {
id: GraphicId::new(1),
width: 10,
height: 10,
color_type: ColorType::Rgb,
pixels: vec![255; 10 * 10 * 3],
is_opaque: true,
resize: None,
display_width: None,
display_height: None,
transmit_time: std::time::Instant::now(),
};
assert!(graphic.is_filled(1, 1, 3, 3));
assert!(!graphic.is_filled(8, 8, 10, 10));
let pixels = {
let mut data = vec![255; 10 * 10 * 4];
for y in 3..6 {
let offset = y * 10 * 4;
data[offset..offset + 3 * 4].fill(0);
}
data
};
let graphic = GraphicData {
id: GraphicId::new(1),
pixels,
width: 10,
height: 10,
color_type: ColorType::Rgba,
is_opaque: false,
resize: None,
display_width: None,
display_height: None,
transmit_time: std::time::Instant::now(),
};
assert!(graphic.is_filled(0, 0, 3, 3));
assert!(!graphic.is_filled(1, 1, 4, 4));
}
#[test]
fn test_graphics_memory_tracking() {
use sugarloaf::ColorType;
let mut graphics = Graphics::default();
let pixels = vec![255u8; 100 * 100 * 4];
let graphic = GraphicData {
id: GraphicId::new(1),
width: 100,
height: 100,
color_type: ColorType::Rgba,
pixels,
is_opaque: true,
resize: None,
display_width: None,
display_height: None,
transmit_time: std::time::Instant::now(),
};
let bytes = Graphics::calculate_graphic_bytes(&graphic);
assert_eq!(bytes, 40_000);
graphics.track_graphic(GraphicId::new(1), bytes);
assert_eq!(graphics.total_bytes, 40_000);
assert!(graphics.image_timestamps.contains_key(&GraphicId::new(1)));
graphics.untrack_graphic(GraphicId::new(1), bytes);
assert_eq!(graphics.total_bytes, 0);
assert!(!graphics.image_timestamps.contains_key(&GraphicId::new(1)));
}
#[test]
fn test_graphics_eviction_unused_first() {
use sugarloaf::ColorType;
let mut graphics = Graphics {
total_limit: 100_000, ..Graphics::default()
};
let mut used_ids = std::collections::HashSet::new();
let pixels1 = vec![255u8; 50_000];
let graphic1 = GraphicData {
id: GraphicId::new(1),
width: 100,
height: 125,
color_type: ColorType::Rgba,
pixels: pixels1.clone(),
is_opaque: true,
resize: None,
display_width: None,
display_height: None,
transmit_time: std::time::Instant::now(),
};
graphics.pending.push(graphic1);
graphics.track_graphic(GraphicId::new(1), pixels1.len());
used_ids.insert(1);
std::thread::sleep(std::time::Duration::from_millis(10));
let pixels2 = vec![255u8; 50_000];
let graphic2 = GraphicData {
id: GraphicId::new(2),
width: 100,
height: 125,
color_type: ColorType::Rgba,
pixels: pixels2.clone(),
is_opaque: true,
resize: None,
display_width: None,
display_height: None,
transmit_time: std::time::Instant::now(),
};
graphics.pending.push(graphic2);
graphics.track_graphic(GraphicId::new(2), pixels2.len());
let pixels3_len = 50_000;
let success = graphics.evict_images(pixels3_len, &used_ids);
assert!(success, "Eviction should succeed");
assert_eq!(graphics.pending.len(), 1);
assert_eq!(graphics.pending[0].id, GraphicId::new(1));
assert!(graphics.image_timestamps.contains_key(&GraphicId::new(1)));
assert!(!graphics.image_timestamps.contains_key(&GraphicId::new(2)));
}
#[test]
fn test_graphics_eviction_oldest_first() {
use sugarloaf::ColorType;
let mut graphics = Graphics {
total_limit: 100_000, ..Graphics::default()
};
let used_ids = std::collections::HashSet::new();
let pixels1 = vec![255u8; 50_000];
let graphic1 = GraphicData {
id: GraphicId::new(1),
width: 100,
height: 125,
color_type: ColorType::Rgba,
pixels: pixels1.clone(),
is_opaque: true,
resize: None,
display_width: None,
display_height: None,
transmit_time: std::time::Instant::now(),
};
graphics.pending.push(graphic1);
graphics.track_graphic(GraphicId::new(1), pixels1.len());
std::thread::sleep(std::time::Duration::from_millis(10));
let pixels2 = vec![255u8; 50_000];
let graphic2 = GraphicData {
id: GraphicId::new(2),
width: 100,
height: 125,
color_type: ColorType::Rgba,
pixels: pixels2.clone(),
is_opaque: true,
resize: None,
display_width: None,
display_height: None,
transmit_time: std::time::Instant::now(),
};
graphics.pending.push(graphic2);
graphics.track_graphic(GraphicId::new(2), pixels2.len());
let pixels3_len = 50_000;
let success = graphics.evict_images(pixels3_len, &used_ids);
assert!(success);
assert_eq!(graphics.pending.len(), 1);
assert_eq!(graphics.pending[0].id, GraphicId::new(2));
}
#[test]
fn test_graphics_eviction_fails_when_not_enough_space() {
use sugarloaf::ColorType;
let mut graphics = Graphics {
total_limit: 100_000, ..Graphics::default()
};
let mut used_ids = std::collections::HashSet::new();
let pixels1 = vec![255u8; 90_000];
let graphic1 = GraphicData {
id: GraphicId::new(1),
width: 150,
height: 150,
color_type: ColorType::Rgba,
pixels: pixels1.clone(),
is_opaque: true,
resize: None,
display_width: None,
display_height: None,
transmit_time: std::time::Instant::now(),
};
graphics.pending.push(graphic1);
graphics.track_graphic(GraphicId::new(1), pixels1.len());
used_ids.insert(1);
let pixels2_len = 90_000;
let success = graphics.evict_images(pixels2_len, &used_ids);
assert!(
success,
"Eviction should succeed by evicting used images if necessary"
);
assert_eq!(graphics.pending.len(), 0);
}
#[test]
fn test_graphics_no_eviction_when_under_limit() {
use sugarloaf::ColorType;
let mut graphics = Graphics {
total_limit: 200_000, ..Graphics::default()
};
let used_ids = std::collections::HashSet::new();
let pixels1 = vec![255u8; 50_000];
let graphic1 = GraphicData {
id: GraphicId::new(1),
width: 100,
height: 125,
color_type: ColorType::Rgba,
pixels: pixels1.clone(),
is_opaque: true,
resize: None,
display_width: None,
display_height: None,
transmit_time: std::time::Instant::now(),
};
graphics.pending.push(graphic1);
graphics.track_graphic(GraphicId::new(1), pixels1.len());
let pixels2_len = 50_000;
let success = graphics.evict_images(pixels2_len, &used_ids);
assert!(success);
assert_eq!(graphics.pending.len(), 1);
assert_eq!(graphics.total_bytes, 50_000);
}