#![allow(clippy::cast_possible_truncation)]
use std::collections::{HashMap, VecDeque};
use std::time::Duration;
pub(crate) struct CachedFrame {
pub rgba: Vec<u8>,
pub width: u32,
pub height: u32,
}
pub(crate) struct FrameCache {
capacity_bytes: usize,
used_bytes: usize,
order: VecDeque<Duration>,
entries: HashMap<Duration, CachedFrame>,
}
impl FrameCache {
pub fn new(capacity_bytes: usize) -> Self {
Self {
capacity_bytes,
used_bytes: 0,
order: VecDeque::new(),
entries: HashMap::new(),
}
}
pub fn get(&self, pts: Duration) -> Option<&CachedFrame> {
self.entries.get(&pts)
}
pub fn insert(&mut self, pts: Duration, rgba: Vec<u8>, w: u32, h: u32) {
if self.entries.contains_key(&pts) {
return;
}
let frame_bytes = (w as usize) * (h as usize) * 4;
while self.used_bytes + frame_bytes > self.capacity_bytes {
let Some(oldest) = self.order.pop_front() else {
break;
};
if let Some(evicted) = self.entries.remove(&oldest) {
let evicted_bytes = (evicted.width as usize) * (evicted.height as usize) * 4;
self.used_bytes = self.used_bytes.saturating_sub(evicted_bytes);
}
}
if frame_bytes > self.capacity_bytes {
return;
}
self.order.push_back(pts);
self.used_bytes += frame_bytes;
self.entries.insert(
pts,
CachedFrame {
rgba,
width: w,
height: h,
},
);
}
pub fn invalidate(&mut self) {
self.entries.clear();
self.order.clear();
self.used_bytes = 0;
}
#[allow(dead_code)]
pub fn used_bytes(&self) -> usize {
self.used_bytes
}
pub fn pts_range(&self) -> Option<(Duration, Duration)> {
let min = self.order.front().copied()?;
let max = self.order.back().copied()?;
Some((min, max))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_cache_get_should_return_none_when_empty() {
let cache = FrameCache::new(1024 * 1024);
assert!(cache.get(Duration::ZERO).is_none());
}
#[test]
fn frame_cache_insert_and_get_should_store_and_retrieve_frame() {
let mut cache = FrameCache::new(1024 * 1024);
let rgba = vec![0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
cache.insert(Duration::from_millis(100), rgba.clone(), 2, 2);
let entry = cache
.get(Duration::from_millis(100))
.expect("inserted frame must be present");
assert_eq!(entry.rgba, rgba);
assert_eq!(entry.width, 2);
assert_eq!(entry.height, 2);
}
#[test]
fn frame_cache_insert_should_evict_oldest_when_over_budget() {
let mut cache = FrameCache::new(32);
cache.insert(Duration::ZERO, vec![0u8; 16], 2, 2);
cache.insert(Duration::from_millis(100), vec![1u8; 16], 2, 2);
assert_eq!(cache.used_bytes(), 32);
cache.insert(Duration::from_millis(200), vec![2u8; 16], 2, 2);
assert!(
cache.get(Duration::ZERO).is_none(),
"oldest frame must be evicted"
);
assert!(cache.get(Duration::from_millis(100)).is_some());
assert!(cache.get(Duration::from_millis(200)).is_some());
assert_eq!(cache.used_bytes(), 32);
}
#[test]
fn frame_cache_invalidate_should_clear_all_entries() {
let mut cache = FrameCache::new(1024);
cache.insert(Duration::ZERO, vec![0u8; 16], 2, 2);
cache.insert(Duration::from_millis(100), vec![0u8; 16], 2, 2);
cache.invalidate();
assert_eq!(cache.used_bytes(), 0);
assert!(cache.get(Duration::ZERO).is_none());
assert!(cache.get(Duration::from_millis(100)).is_none());
}
#[test]
fn frame_cache_pts_range_should_return_none_when_empty() {
let cache = FrameCache::new(1024);
assert!(cache.pts_range().is_none());
}
#[test]
fn frame_cache_pts_range_should_return_min_and_max_pts() {
let mut cache = FrameCache::new(1024 * 1024);
cache.insert(Duration::ZERO, vec![0u8; 16], 2, 2);
cache.insert(Duration::from_millis(100), vec![0u8; 16], 2, 2);
cache.insert(Duration::from_millis(200), vec![0u8; 16], 2, 2);
let (min, max) = cache.pts_range().unwrap();
assert_eq!(min, Duration::ZERO);
assert_eq!(max, Duration::from_millis(200));
}
#[test]
fn frame_cache_duplicate_pts_insert_should_be_a_no_op() {
let mut cache = FrameCache::new(1024);
let first = vec![1u8; 16];
let second = vec![2u8; 16];
cache.insert(Duration::ZERO, first.clone(), 2, 2);
cache.insert(Duration::ZERO, second, 2, 2);
let entry = cache.get(Duration::ZERO).unwrap();
assert_eq!(
entry.rgba, first,
"duplicate insert must be a no-op; first frame must remain"
);
assert_eq!(cache.used_bytes(), 16);
}
#[test]
fn frame_cache_oversized_frame_should_not_be_inserted() {
let mut cache = FrameCache::new(10); cache.insert(Duration::ZERO, vec![0u8; 16], 2, 2);
assert!(
cache.get(Duration::ZERO).is_none(),
"oversized frame must not be inserted"
);
assert_eq!(cache.used_bytes(), 0);
}
#[test]
#[ignore = "performance thresholds are environment-dependent; run explicitly with -- --include-ignored"]
fn frame_cache_scrub_latency_with_cache_should_be_faster_than_without() {
}
}