use std::collections::HashMap;
pub const QUALITY_BLOCK: usize = 16;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct TileKey {
pub canvas: String,
pub level: u32,
pub x: i32,
pub y: i32,
}
impl TileKey {
pub fn new(canvas: &str, level: u32, x: i32, y: i32) -> Self {
Self {
canvas: canvas.to_string(),
level,
x,
y,
}
}
pub fn path(&self) -> String {
format!("{}/{}/{}_{}", self.canvas, self.level, self.x, self.y)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Tile {
pub key: TileKey,
pub pixels: Vec<u8>,
pub coverage: Vec<u8>,
pub provisional: Vec<u8>,
pub quality: Vec<u8>,
pub conflicts: Vec<u8>,
pub owner: Vec<u32>,
pub score: Vec<f32>,
pub frozen: Vec<u8>,
pub dirty: bool,
pub existed: bool,
pub touched: Option<f64>,
id: u32,
last_use: u64,
}
impl Tile {
pub fn id(&self) -> u32 {
self.id
}
pub fn observed(&self) -> u64 {
count_covered(&self.coverage)
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct StoredTile {
pub pixels: Vec<u8>,
pub coverage: Option<Vec<u8>>,
pub provisional: Option<Vec<u8>>,
pub quality: Option<Vec<u8>>,
pub conflicts: Option<Vec<u8>>,
pub owner: Option<Vec<u32>>,
pub score: Option<Vec<f32>>,
pub frozen: Option<Vec<u8>>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TileCacheStats {
pub encoded: u64,
pub decoded: u64,
pub evictions: u64,
pub peak_resident: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TileError {
BadSize,
BadPixels,
Resident,
}
pub fn count_covered(coverage: &[u8]) -> u64 {
coverage.iter().map(|b| b.count_ones() as u64).sum()
}
const BYTES_PER_PIXEL: f64 = 4.3;
#[derive(Debug)]
pub struct TileCache {
size: usize,
max_tiles: u64,
tiles: HashMap<u32, Tile>,
by_key: HashMap<TileKey, u32>,
clock: u64,
next_id: u32,
stats: TileCacheStats,
}
impl TileCache {
pub fn new(size: usize, memory_mb: f64) -> Result<Self, TileError> {
if size < QUALITY_BLOCK || !size.is_multiple_of(QUALITY_BLOCK) {
return Err(TileError::BadSize);
}
let per_tile = (size * size) as f64 * BYTES_PER_PIXEL;
let max_tiles = ((memory_mb * 1024.0 * 1024.0 * 0.30 / per_tile).floor() as u64).max(2);
Ok(Self {
size,
max_tiles,
tiles: HashMap::new(),
by_key: HashMap::new(),
clock: 0,
next_id: 0,
stats: TileCacheStats::default(),
})
}
pub fn size(&self) -> usize {
self.size
}
pub fn max_tiles(&self) -> u64 {
self.max_tiles
}
pub fn set_max_tiles(&mut self, max_tiles: u64) {
self.max_tiles = max_tiles;
}
pub fn stats(&self) -> TileCacheStats {
self.stats
}
pub fn len(&self) -> usize {
self.tiles.len()
}
pub fn is_empty(&self) -> bool {
self.tiles.is_empty()
}
pub fn configure_budget(&mut self, memory_mb: f64, reserved_bytes: f64) {
let available = (memory_mb * 1024.0 * 1024.0 - reserved_bytes).max(0.0);
let per_tile = (self.size * self.size) as f64 * BYTES_PER_PIXEL;
self.max_tiles = ((available * 0.8 / per_tile).floor() as u64).max(2);
}
pub fn footprint(&self, width: f64, height: f64) -> u64 {
let size = self.size as f64;
((width / size).ceil() as u64 + 1) * ((height / size).ceil() as u64 + 1)
}
pub fn ensure_footprint(&mut self, areas: &[(f64, f64)]) -> u64 {
let needed = areas
.iter()
.map(|&(w, h)| self.footprint(w, h))
.sum::<u64>()
+ 2;
if self.max_tiles >= needed {
return 0;
}
let added = needed - self.max_tiles;
self.max_tiles = needed;
added
}
pub fn is_resident(&self, key: &TileKey) -> bool {
self.by_key.contains_key(key)
}
pub fn touch(&mut self, key: &TileKey) -> Option<u32> {
let id = *self.by_key.get(key)?;
self.clock += 1;
self.tiles.get_mut(&id).unwrap().last_use = self.clock;
Some(id)
}
pub fn victim(&self) -> Option<u32> {
if (self.tiles.len() as u64) < self.max_tiles {
return None;
}
self.tiles.values().min_by_key(|t| t.last_use).map(|t| t.id)
}
pub fn evict(&mut self, id: u32) -> Option<Tile> {
let tile = self.tiles.remove(&id)?;
self.by_key.remove(&tile.key);
self.stats.evictions += 1;
Some(tile)
}
pub fn insert(&mut self, key: TileKey, stored: Option<StoredTile>) -> Result<u32, TileError> {
if self.by_key.contains_key(&key) {
return Err(TileError::Resident);
}
let n = self.size * self.size;
let bits = n.div_ceil(8);
let blocks = (self.size / QUALITY_BLOCK) * (self.size / QUALITY_BLOCK);
let existed = stored.is_some();
let s = stored.unwrap_or_else(|| StoredTile {
pixels: vec![0; n * 4],
..StoredTile::default()
});
if s.pixels.len() != n * 4 {
return Err(TileError::BadPixels);
}
if existed {
self.stats.decoded += 1;
}
self.next_id += 1;
self.clock += 1;
let id = self.next_id;
let tile = Tile {
key: key.clone(),
pixels: s.pixels,
coverage: s.coverage.unwrap_or_else(|| vec![0; bits]),
provisional: s.provisional.unwrap_or_else(|| vec![0; bits]),
quality: s.quality.unwrap_or_else(|| vec![0; blocks]),
conflicts: s.conflicts.unwrap_or_else(|| vec![0; blocks]),
owner: s.owner.unwrap_or_else(|| vec![0; blocks]),
score: s.score.unwrap_or_else(|| vec![0.0; blocks]),
frozen: s.frozen.unwrap_or_else(|| vec![0; blocks]),
dirty: false,
existed,
touched: None,
id,
last_use: self.clock,
};
self.tiles.insert(id, tile);
self.by_key.insert(key, id);
self.stats.peak_resident = self.stats.peak_resident.max(self.tiles.len() as u64);
Ok(id)
}
pub fn get(&self, id: u32) -> Option<&Tile> {
self.tiles.get(&id)
}
pub fn get_mut(&mut self, id: u32) -> Option<&mut Tile> {
self.tiles.get_mut(&id)
}
pub fn id_of(&self, key: &TileKey) -> Option<u32> {
self.by_key.get(key).copied()
}
pub fn mark_dirty(&mut self, id: u32, now: f64) {
if let Some(t) = self.tiles.get_mut(&id) {
t.dirty = true;
t.touched = Some(now);
}
}
pub fn dirty(&self, settled_before: Option<f64>) -> Vec<u32> {
let mut out: Vec<&Tile> = self
.tiles
.values()
.filter(|t| t.dirty && settled_before.is_none_or(|s| t.touched.unwrap_or(0.0) < s))
.collect();
out.sort_by_key(|t| t.last_use);
out.into_iter().map(|t| t.id).collect()
}
pub fn encoded(&mut self) {
self.stats.encoded += 1;
}
pub fn saved(&mut self, id: u32) {
if let Some(t) = self.tiles.get_mut(&id) {
t.dirty = false;
t.existed = true;
}
}
pub fn clear(&mut self) {
self.tiles.clear();
self.by_key.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key(x: i32) -> TileKey {
TileKey::new("c", 0, x, 0)
}
#[test]
fn rejects_a_size_that_is_not_a_block_multiple() {
assert_eq!(TileCache::new(0, 128.0).unwrap_err(), TileError::BadSize);
assert_eq!(TileCache::new(24, 128.0).unwrap_err(), TileError::BadSize);
assert!(TileCache::new(16, 128.0).is_ok());
}
#[test]
fn budget_never_drops_below_two_tiles() {
let mut cache = TileCache::new(512, 0.0).unwrap();
assert_eq!(cache.max_tiles(), 2);
cache.configure_budget(1.0, 1e12);
assert_eq!(cache.max_tiles(), 2);
}
#[test]
fn budget_matches_the_page_formula() {
let mut cache = TileCache::new(512, 128.0).unwrap();
assert_eq!(cache.max_tiles(), 35);
cache.configure_budget(256.0, 100.0 * 1024.0 * 1024.0);
assert_eq!(cache.max_tiles(), 116);
}
#[test]
fn footprint_floor_only_raises_the_limit() {
let mut cache = TileCache::new(512, 128.0).unwrap();
cache.set_max_tiles(10);
assert_eq!(
cache.ensure_footprint(&[(1000.0, 600.0), (100.0, 100.0)]),
5
);
assert_eq!(cache.max_tiles(), 15);
assert_eq!(cache.ensure_footprint(&[(100.0, 100.0)]), 0);
assert_eq!(cache.max_tiles(), 15);
}
#[test]
fn a_stored_tile_with_the_wrong_pixel_count_is_refused() {
let mut cache = TileCache::new(16, 128.0).unwrap();
let stored = StoredTile {
pixels: vec![0; 10],
..StoredTile::default()
};
assert_eq!(
cache.insert(key(0), Some(stored)),
Err(TileError::BadPixels)
);
assert!(cache.is_empty());
assert_eq!(cache.stats().decoded, 0);
}
#[test]
fn inserting_a_resident_key_is_refused() {
let mut cache = TileCache::new(16, 128.0).unwrap();
cache.insert(key(0), None).unwrap();
assert_eq!(cache.insert(key(0), None), Err(TileError::Resident));
}
#[test]
fn eviction_takes_the_least_recently_used_tile_and_only_when_full() {
let mut cache = TileCache::new(16, 128.0).unwrap();
cache.set_max_tiles(2);
assert!(cache.victim().is_none());
cache.insert(key(0), None).unwrap();
cache.insert(key(1), None).unwrap();
let a = cache.touch(&key(0)).unwrap();
let b = cache.victim().unwrap();
let out = cache.evict(b).unwrap();
assert_eq!(out.key, key(1));
assert!(cache.victim().is_none());
assert_eq!(cache.stats().evictions, 1);
assert!(!cache.is_resident(&key(1)));
assert!(cache.get(a).is_some());
}
#[test]
fn naming_the_victim_changes_nothing() {
let mut cache = TileCache::new(16, 128.0).unwrap();
cache.set_max_tiles(2);
let a = cache.insert(key(0), None).unwrap();
cache.insert(key(1), None).unwrap();
cache.mark_dirty(a, 1.0);
assert_eq!(cache.victim(), Some(a));
assert_eq!(cache.victim(), Some(a));
assert!(cache.is_resident(&key(0)));
assert!(cache.get(a).unwrap().dirty);
assert_eq!(cache.len(), 2);
assert_eq!(cache.stats().evictions, 0);
cache.saved(a);
let out = cache.evict(a).unwrap();
assert!(!out.dirty && out.existed);
assert_eq!(cache.stats().evictions, 1);
assert_eq!(cache.victim(), None);
}
#[test]
fn evicting_a_tile_that_is_not_resident_does_nothing() {
let mut cache = TileCache::new(16, 128.0).unwrap();
cache.set_max_tiles(1);
let a = cache.insert(key(0), None).unwrap();
assert!(cache.evict(a + 1).is_none());
assert!(cache.evict(a).is_some());
assert!(cache.evict(a).is_none());
assert_eq!(cache.stats().evictions, 1);
}
#[test]
fn eviction_with_an_empty_cache_and_a_zero_limit_stops() {
let mut cache = TileCache::new(16, 128.0).unwrap();
cache.set_max_tiles(0);
assert!(cache.victim().is_none());
}
#[test]
fn missing_planes_start_empty_and_a_stored_tile_counts_as_decoded() {
let mut cache = TileCache::new(32, 128.0).unwrap();
let id = cache
.insert(
key(0),
Some(StoredTile {
pixels: vec![7; 32 * 32 * 4],
owner: Some(vec![]),
..StoredTile::default()
}),
)
.unwrap();
let t = cache.get(id).unwrap();
assert!(t.existed && !t.dirty);
assert_eq!(t.coverage.len(), 128);
assert_eq!(t.quality.len(), 4);
assert!(t.owner.is_empty());
assert_eq!(cache.stats().decoded, 1);
assert_eq!(cache.stats().peak_resident, 1);
}
#[test]
fn settled_flush_skips_tiles_written_since_the_previous_flush() {
let mut cache = TileCache::new(16, 128.0).unwrap();
let a = cache.insert(key(0), None).unwrap();
let b = cache.insert(key(1), None).unwrap();
let c = cache.insert(key(2), None).unwrap();
cache.mark_dirty(a, 10.0);
cache.mark_dirty(b, 30.0);
cache.mark_dirty(c, 20.0);
cache.touch(&key(0));
assert_eq!(cache.dirty(Some(25.0)), vec![c, a]);
assert_eq!(cache.dirty(Some(20.0)), vec![a]);
assert_eq!(cache.dirty(None), vec![b, c, a]);
cache.saved(a);
assert_eq!(cache.dirty(None), vec![b, c]);
assert!(cache.get(a).unwrap().existed);
}
#[test]
fn untouched_dirty_tile_reads_as_written_at_zero() {
let mut cache = TileCache::new(16, 128.0).unwrap();
let a = cache.insert(key(0), None).unwrap();
cache.get_mut(a).unwrap().dirty = true;
assert_eq!(cache.dirty(Some(0.0)), Vec::<u32>::new());
assert_eq!(cache.dirty(Some(1.0)), vec![a]);
}
#[test]
fn a_reloaded_tile_gets_a_new_id() {
let mut cache = TileCache::new(16, 128.0).unwrap();
cache.set_max_tiles(1);
let a = cache.insert(key(0), None).unwrap();
let out = cache.evict(cache.victim().unwrap()).unwrap();
let b = cache.insert(key(0), None).unwrap();
assert_ne!(a, b);
assert_eq!(out.id(), a);
assert!(cache.get(a).is_none());
}
}