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 std::mem;
use std::sync::Arc;
use tracing::debug;
#[derive(Debug, Clone)]
pub struct UpdateQueues {
pub pending: Vec<GraphicData>,
pub pending_images: Vec<(u32, GraphicData)>,
pub remove_queue: Vec<u64>,
}
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 requested_columns: u32,
pub requested_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,
}
pub fn resolve_source_rect(
source_x: u32,
source_y: u32,
source_width: u32,
source_height: u32,
image_width: usize,
image_height: usize,
) -> Option<(usize, usize, usize, usize)> {
if image_width == 0 || image_height == 0 {
return None;
}
let x = (source_x as usize).min(image_width);
let y = (source_y as usize).min(image_height);
let width = if source_width > 0 {
(source_width as usize).min(image_width - x)
} else {
image_width - x
};
let height = if source_height > 0 {
(source_height as usize).min(image_height - y)
} else {
image_height - y
};
if width == 0 || height == 0 {
return None;
}
Some((x, y, width, height))
}
pub fn kitty_display_size(
source_width: usize,
source_height: usize,
requested_columns: u32,
requested_rows: u32,
cell_width: usize,
cell_height: usize,
) -> (usize, usize) {
if source_width == 0 || source_height == 0 {
return (0, 0);
}
match (requested_columns, requested_rows) {
(0, 0) => (source_width, source_height),
(c, 0) => {
let w = c as usize * cell_width;
let h =
(source_height as f64 * w as f64 / source_width as f64).round() as usize;
(w, h)
}
(0, r) => {
let h = r as usize * cell_height;
let w =
(source_width as f64 * h as f64 / source_height as f64).round() as usize;
(w, h)
}
(c, r) => (c as usize * cell_width, r as usize * cell_height),
}
}
impl KittyPlacement {
pub fn rescale(
&mut self,
image_width: usize,
image_height: usize,
cell_width: usize,
cell_height: usize,
) {
if cell_width == 0 || cell_height == 0 {
return;
}
let Some((_, _, source_width, source_height)) = resolve_source_rect(
self.source_x,
self.source_y,
self.source_width,
self.source_height,
image_width,
image_height,
) else {
self.pixel_width = 0;
self.pixel_height = 0;
self.columns = 0;
self.rows = 0;
return;
};
let (w, h) = kitty_display_size(
source_width,
source_height,
self.requested_columns,
self.requested_rows,
cell_width,
cell_height,
);
if w == 0 || h == 0 {
return;
}
self.pixel_width = w as u32;
self.pixel_height = h as u32;
let x_offset = (self.cell_x_offset as usize).min(cell_width - 1);
let y_offset = (self.cell_y_offset as usize).min(cell_height - 1);
self.columns = if self.requested_columns > 0 {
self.requested_columns
} else {
(w + x_offset).div_ceil(cell_width) as u32
};
self.rows = if self.requested_rows > 0 {
self.requested_rows
} else {
(h + y_offset).div_ceil(cell_height) as u32
};
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct KittyOverlayGeometry {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub source_rect: [f32; 4],
}
pub fn clip_overlay_to_rect(
overlay: &mut crate::sugarloaf::GraphicOverlay,
clip_x0: f32,
clip_y0: f32,
clip_x1: f32,
clip_y1: f32,
) -> bool {
if overlay.width <= 0.0 || overlay.height <= 0.0 {
return false;
}
let x0 = overlay.x;
let y0 = overlay.y;
let x1 = overlay.x + overlay.width;
let y1 = overlay.y + overlay.height;
let nx0 = x0.max(clip_x0);
let ny0 = y0.max(clip_y0);
let nx1 = x1.min(clip_x1);
let ny1 = y1.min(clip_y1);
if nx1 <= nx0 || ny1 <= ny0 {
return false;
}
let [u0, v0, u1, v1] = overlay.source_rect;
let fx0 = (nx0 - x0) / overlay.width;
let fx1 = (nx1 - x0) / overlay.width;
let fy0 = (ny0 - y0) / overlay.height;
let fy1 = (ny1 - y0) / overlay.height;
overlay.source_rect = [
u0 + (u1 - u0) * fx0,
v0 + (v1 - v0) * fy0,
u0 + (u1 - u0) * fx1,
v0 + (v1 - v0) * fy1,
];
overlay.x = nx0;
overlay.y = ny0;
overlay.width = nx1 - nx0;
overlay.height = ny1 - ny0;
true
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OverlayViewport {
pub cell_width: f32,
pub cell_height: f32,
pub origin_x: f32,
pub origin_y: f32,
pub history_size: i64,
pub display_offset: i64,
pub screen_lines: i64,
}
pub fn kitty_overlay_geometry(
placement: &KittyPlacement,
image_width: usize,
image_height: usize,
viewport: &OverlayViewport,
) -> Option<KittyOverlayGeometry> {
let screen_row =
placement.dest_row - (viewport.history_size - viewport.display_offset);
let bottom_row = screen_row + placement.rows as i64;
if bottom_row <= 0 || screen_row >= viewport.screen_lines {
return None;
}
let (source_x, source_y, source_width, source_height) = resolve_source_rect(
placement.source_x,
placement.source_y,
placement.source_width,
placement.source_height,
image_width,
image_height,
)?;
let cell_width_px = viewport.cell_width.round() as usize;
let cell_height_px = viewport.cell_height.round() as usize;
if cell_width_px == 0 || cell_height_px == 0 {
return None;
}
let (display_width, display_height) = kitty_display_size(
source_width,
source_height,
placement.requested_columns,
placement.requested_rows,
cell_width_px,
cell_height_px,
);
if display_width == 0 || display_height == 0 {
return None;
}
let image_width = image_width as f32;
let image_height = image_height as f32;
let source_rect = [
source_x as f32 / image_width,
source_y as f32 / image_height,
(source_x + source_width) as f32 / image_width,
(source_y + source_height) as f32 / image_height,
];
let x_offset = (placement.cell_x_offset as f32).min(viewport.cell_width - 1.0);
let y_offset = (placement.cell_y_offset as f32).min(viewport.cell_height - 1.0);
Some(KittyOverlayGeometry {
x: viewport.origin_x + placement.dest_col as f32 * viewport.cell_width + x_offset,
y: viewport.origin_y + screen_row as f32 * viewport.cell_height + y_offset,
width: display_width as f32,
height: display_height as f32,
source_rect,
})
}
#[derive(Debug, Clone, PartialEq)]
pub struct AtlasPlacement {
pub image_key: u64,
pub abs_row: i64,
pub col: usize,
pub columns: usize,
pub rows: usize,
pub src_x: u32,
pub src_y: u32,
pub src_width: u32,
pub src_height: u32,
pub total_width: u32,
pub total_height: u32,
pub insert_cell_w: u16,
pub insert_cell_h: u16,
}
impl AtlasPlacement {
pub(crate) fn slice(
&self,
row0: i64,
row1: i64,
col0: usize,
col1: usize,
) -> AtlasPlacement {
let icw = self.insert_cell_w as u32;
let ich = self.insert_cell_h as u32;
let src_x0 = self.src_x + (col0 - self.col) as u32 * icw;
let src_y0 = self.src_y + (row0 - self.abs_row) as u32 * ich;
let src_x1 = (self.src_x + (col1 - self.col) as u32 * icw)
.min(self.src_x + self.src_width);
let src_y1 = (self.src_y + (row1 - self.abs_row) as u32 * ich)
.min(self.src_y + self.src_height);
AtlasPlacement {
image_key: self.image_key,
abs_row: row0,
col: col0,
columns: col1 - col0,
rows: (row1 - row0) as usize,
src_x: src_x0,
src_y: src_y0,
src_width: src_x1.saturating_sub(src_x0),
src_height: src_y1.saturating_sub(src_y0),
total_width: self.total_width,
total_height: self.total_height,
insert_cell_w: self.insert_cell_w,
insert_cell_h: self.insert_cell_h,
}
}
pub fn subtract_rect(
&self,
hr0: i64,
hr1: i64,
hc0: usize,
hc1: usize,
out: &mut Vec<AtlasPlacement>,
) -> Option<()> {
let p_r0 = self.abs_row;
let p_r1 = self.abs_row + self.rows as i64;
let p_c0 = self.col;
let p_c1 = self.col + self.columns;
if hr1 <= p_r0 || hr0 >= p_r1 || hc1 <= p_c0 || hc0 >= p_c1 {
return None;
}
let ir0 = hr0.max(p_r0);
let ir1 = hr1.min(p_r1);
let mut push = |child: AtlasPlacement| {
if child.columns > 0
&& child.rows > 0
&& child.src_width > 0
&& child.src_height > 0
{
out.push(child);
}
};
if ir0 > p_r0 {
push(self.slice(p_r0, ir0, p_c0, p_c1));
}
if ir1 < p_r1 {
push(self.slice(ir1, p_r1, p_c0, p_c1));
}
if hc0 > p_c0 {
push(self.slice(ir0, ir1, p_c0, hc0.min(p_c1)));
}
if hc1 < p_c1 {
push(self.slice(ir0, ir1, hc1.max(p_c0), p_c1));
}
Some(())
}
}
pub fn atlas_overlay_geometry(
placement: &AtlasPlacement,
viewport: &OverlayViewport,
) -> Option<KittyOverlayGeometry> {
let screen_row =
placement.abs_row - (viewport.history_size - viewport.display_offset);
let bottom_row = screen_row + placement.rows as i64;
if bottom_row <= 0 || screen_row >= viewport.screen_lines {
return None;
}
if placement.src_width == 0
|| placement.src_height == 0
|| placement.total_width == 0
|| placement.total_height == 0
{
return None;
}
let scale_x = viewport.cell_width / placement.insert_cell_w.max(1) as f32;
let scale_y = viewport.cell_height / placement.insert_cell_h.max(1) as f32;
let total_w = placement.total_width as f32;
let total_h = placement.total_height as f32;
Some(KittyOverlayGeometry {
x: viewport.origin_x + placement.col as f32 * viewport.cell_width,
y: viewport.origin_y + screen_row as f32 * viewport.cell_height,
width: placement.src_width as f32 * scale_x,
height: placement.src_height as f32 * scale_y,
source_rect: [
placement.src_x as f32 / total_w,
placement.src_y as f32 / total_h,
(placement.src_x + placement.src_width) as f32 / total_w,
(placement.src_y + placement.src_height) as f32 / total_h,
],
})
}
#[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,
pub width: u32,
pub height: u32,
}
#[derive(Debug, Default)]
pub struct KittyScreenState {
pub kitty_images: FxHashMap<u32, StoredImage>,
pub kitty_image_numbers: FxHashMap<u32, u32>,
pub kitty_placements: FxHashMap<(u32, u32), KittyPlacement>,
pub kitty_virtual_placements: FxHashMap<(u32, u32), VirtualPlacement>,
pub atlas_placements: Vec<AtlasPlacement>,
pub atlas_key_refs: FxHashMap<u64, 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<u64>>>,
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 kitty_placements: FxHashMap<(u32, u32), KittyPlacement>,
pub atlas_placements: Vec<AtlasPlacement>,
pub atlas_key_refs: FxHashMap<u64, u32>,
pub kitty_inactive_screen: KittyScreenState,
pub next_internal_placement_id: u32,
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(),
kitty_placements: FxHashMap::default(),
atlas_placements: Vec::new(),
atlas_key_refs: FxHashMap::default(),
kitty_inactive_screen: KittyScreenState::default(),
next_internal_placement_id: 0,
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 allocate_internal_placement_id(&mut self) -> u32 {
if self.next_internal_placement_id < 0x80000000 {
self.next_internal_placement_id = 0x80000000;
}
let id = self.next_internal_placement_id;
self.next_internal_placement_id = self
.next_internal_placement_id
.checked_add(1)
.unwrap_or(0x80000000);
id
}
pub fn recount_atlas_keys(&mut self) {
Self::recount_atlas_keys_for(
&self.atlas_placements,
&mut self.atlas_key_refs,
&self.texture_operations,
);
}
pub fn recount_inactive_atlas_keys(&mut self) {
Self::recount_atlas_keys_for(
&self.kitty_inactive_screen.atlas_placements,
&mut self.kitty_inactive_screen.atlas_key_refs,
&self.texture_operations,
);
}
fn recount_atlas_keys_for(
placements: &[AtlasPlacement],
key_refs: &mut FxHashMap<u64, u32>,
texture_operations: &std::sync::Arc<parking_lot::Mutex<Vec<u64>>>,
) {
let mut refs: FxHashMap<u64, u32> = FxHashMap::default();
for placement in placements {
*refs.entry(placement.image_key).or_insert(0) += 1;
}
let mut removals = texture_operations.lock();
for key in key_refs.keys() {
if !refs.contains_key(key) {
removals.push(*key);
}
}
drop(removals);
*key_refs = refs;
}
pub fn swap_kitty_screen_state(&mut self) {
std::mem::swap(
&mut self.kitty_images,
&mut self.kitty_inactive_screen.kitty_images,
);
std::mem::swap(
&mut self.kitty_image_numbers,
&mut self.kitty_inactive_screen.kitty_image_numbers,
);
std::mem::swap(
&mut self.kitty_placements,
&mut self.kitty_inactive_screen.kitty_placements,
);
std::mem::swap(
&mut self.kitty_virtual_placements,
&mut self.kitty_inactive_screen.kitty_virtual_placements,
);
std::mem::swap(
&mut self.atlas_placements,
&mut self.kitty_inactive_screen.atlas_placements,
);
std::mem::swap(
&mut self.atlas_key_refs,
&mut self.kitty_inactive_screen.atlas_key_refs,
);
self.kitty_graphics_dirty = true;
}
pub fn clear_all_kitty_state(&mut self) {
let inactive_bytes: usize = self
.kitty_inactive_screen
.kitty_images
.values()
.map(|s| s.data.pixels.len())
.sum();
self.total_bytes = self.total_bytes.saturating_sub(inactive_bytes);
self.kitty_images.clear();
self.kitty_image_numbers.clear();
self.kitty_placements.clear();
self.kitty_virtual_placements.clear();
{
let mut removals = self.texture_operations.lock();
for key in self.atlas_key_refs.keys() {
removals.push(*key);
}
for key in self.kitty_inactive_screen.atlas_key_refs.keys() {
removals.push(*key);
}
}
self.atlas_placements.clear();
self.atlas_key_refs.clear();
self.kitty_inactive_screen = KittyScreenState::default();
self.kitty_graphics_dirty = true;
}
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;
self.kitty_graphics_dirty = true;
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,
) {
let before = self.kitty_images.len();
self.kitty_images.retain(|id, img| !predicate(id, img));
if self.kitty_images.len() != before {
self.kitty_graphics_dirty = true;
}
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);
#[derive(Copy, Clone, PartialEq, Eq)]
enum CandidateSource {
Pending,
ActiveKitty,
InactiveKitty,
}
fn tier_for(source: CandidateSource, is_used: bool) -> u8 {
match source {
CandidateSource::InactiveKitty => 0,
CandidateSource::Pending | CandidateSource::ActiveKitty => {
if is_used {
2
} else {
1
}
}
}
}
let mut candidates: Vec<(
GraphicId,
std::time::Instant,
bool,
usize,
CandidateSource,
)> = 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());
if is_used {
continue;
}
let bytes = Self::calculate_graphic_bytes(graphic);
candidates.push((
graphic.id,
timestamp,
is_used,
bytes,
CandidateSource::Pending,
));
}
}
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,
CandidateSource::ActiveKitty,
));
}
for (&kitty_id, stored) in &self.kitty_inactive_screen.kitty_images {
let id_as_u64 = kitty_id as u64;
let bytes = Self::calculate_graphic_bytes(&stored.data);
candidates.push((
GraphicId::new(id_as_u64),
stored.transmission_time,
false, bytes,
CandidateSource::InactiveKitty,
));
}
if candidates.is_empty() {
debug!("No candidates for eviction");
return false;
}
candidates.sort_by(|a, b| {
let ta = tier_for(a.4, a.2);
let tb = tier_for(b.4, b.2);
ta.cmp(&tb).then_with(|| a.1.cmp(&b.1))
});
let mut freed_bytes = 0usize;
let mut evicted: Vec<(GraphicId, CandidateSource)> = Vec::new();
for (graphic_id, _, is_used, bytes, source) in candidates {
if freed_bytes >= bytes_to_free {
break;
}
evicted.push((graphic_id, source));
freed_bytes += bytes;
debug!(
"Evicting graphic id={}, bytes={}, used={}",
graphic_id.get(),
bytes,
is_used
);
}
for (id, source) in evicted {
let evicted_u32 = id.get() as u32;
match source {
CandidateSource::Pending => {
self.pending.retain(|g| g.id != id);
}
CandidateSource::ActiveKitty => {
self.kitty_images.remove(&evicted_u32);
self.kitty_image_numbers.retain(|_, v| *v != evicted_u32);
self.kitty_graphics_dirty = true;
}
CandidateSource::InactiveKitty => {
self.kitty_inactive_screen.kitty_images.remove(&evicted_u32);
self.kitty_inactive_screen
.kitty_image_numbers
.retain(|_, v| *v != evicted_u32);
}
}
self.image_timestamps.remove(&id);
let key = match source {
CandidateSource::Pending => crate::sugarloaf::atlas_image_key(id.get()),
CandidateSource::ActiveKitty | CandidateSource::InactiveKitty => {
crate::sugarloaf::kitty_image_key(id.get() as u32)
}
};
self.texture_operations.lock().push(key);
}
let active_ids: std::collections::HashSet<u32> =
self.kitty_images.keys().copied().collect();
self.kitty_placements
.retain(|_, p| active_ids.contains(&p.image_id));
let inactive_ids: std::collections::HashSet<u32> = self
.kitty_inactive_screen
.kitty_images
.keys()
.copied()
.collect();
self.kitty_inactive_screen
.kitty_placements
.retain(|_, p| inactive_ids.contains(&p.image_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 collect_active_graphic_ids(&mut self) -> std::collections::HashSet<u64> {
let mut active = std::collections::HashSet::new();
for placement in &self.atlas_placements {
active.insert(placement.image_key - (1u64 << 32));
}
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,
"no evictable candidates: the pending image is in use"
);
assert_eq!(graphics.pending.len(), 1, "used pending image survives");
}
#[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);
}