use crate::db::Database;
use crossbeam_channel::{Receiver, Sender};
use egui::{ColorImage, TextureHandle, TextureOptions};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
const THUMB_UPLOADS_PER_FRAME: usize = 48;
const FULL_UPLOADS_PER_FRAME: usize = 6;
const WORKERS: usize = 4;
enum Request {
Thumb {
generation: u64,
idx: usize,
bytes: Arc<[u8]>,
},
Full {
generation: u64,
idx: usize,
rel_path: String,
},
}
struct Decoded {
generation: u64,
idx: usize,
full: bool,
image: ColorImage,
}
fn decode(bytes: &[u8]) -> Option<ColorImage> {
let rgba = image::load_from_memory(bytes).ok()?.to_rgba8();
let size = [rgba.width() as usize, rgba.height() as usize];
Some(ColorImage::from_rgba_unmultiplied(size, rgba.as_raw()))
}
pub struct BgCache {
generation: u64,
req_tx: Sender<Request>,
res_rx: Receiver<Decoded>,
thumbs: Vec<Option<TextureHandle>>,
thumb_ready: Vec<(usize, ColorImage)>,
gpu: HashMap<usize, TextureHandle>,
ram: HashMap<usize, ColorImage>,
full_ready: Vec<(usize, ColorImage)>,
inflight_full: HashSet<usize>,
inflight_thumbs: usize,
released: bool,
}
impl BgCache {
pub fn new() -> Self {
let (req_tx, req_rx) = crossbeam_channel::unbounded::<Request>();
let (res_tx, res_rx) = crossbeam_channel::unbounded::<Decoded>();
for w in 0..WORKERS {
let req_rx = req_rx.clone();
let res_tx = res_tx.clone();
let _ = std::thread::Builder::new()
.name(format!("pinready-bgdecode-{w}"))
.spawn(move || {
let mut db: Option<Database> = None;
while let Ok(req) = req_rx.recv() {
let done = match req {
Request::Thumb {
generation,
idx,
bytes,
} => decode(&bytes).map(|image| Decoded {
generation,
idx,
full: false,
image,
}),
Request::Full {
generation,
idx,
rel_path,
} => {
if db.is_none() {
db = Database::open(None).ok();
}
db.as_ref()
.and_then(|d| d.get_backglass(&rel_path))
.and_then(|(bytes, _)| decode(&bytes))
.map(|image| Decoded {
generation,
idx,
full: true,
image,
})
}
};
let msg = done.unwrap_or_else(|| Decoded {
generation: u64::MAX,
idx: usize::MAX,
full: false,
image: ColorImage::default(),
});
if res_tx.send(msg).is_err() {
break;
}
}
});
}
Self {
generation: 0,
req_tx,
res_rx,
thumbs: Vec::new(),
thumb_ready: Vec::new(),
gpu: HashMap::new(),
ram: HashMap::new(),
full_ready: Vec::new(),
inflight_full: HashSet::new(),
inflight_thumbs: 0,
released: false,
}
}
pub fn reset(&mut self, generation: u64, tables: usize) {
self.generation = generation;
self.thumbs = vec![None; tables];
self.thumb_ready.clear();
self.gpu.clear();
self.ram.clear();
self.full_ready.clear();
self.inflight_full.clear();
self.inflight_thumbs = 0;
}
pub fn release(&mut self, keep: Option<usize>) {
self.generation += 1;
let kept = keep.and_then(|idx| self.gpu.remove(&idx).map(|tex| (idx, tex)));
self.thumbs.iter_mut().for_each(|t| *t = None);
self.thumb_ready.clear();
self.gpu.clear();
self.ram.clear();
self.full_ready.clear();
self.inflight_full.clear();
self.inflight_thumbs = 0;
self.released = true;
if let Some((idx, tex)) = kept {
self.gpu.insert(idx, tex);
}
}
pub fn restore(&mut self) {
self.released = false;
}
pub fn is_released(&self) -> bool {
self.released
}
pub fn queue_thumb(&mut self, idx: usize, bytes: Arc<[u8]>) {
self.inflight_thumbs += 1;
let _ = self.req_tx.send(Request::Thumb {
generation: self.generation,
idx,
bytes,
});
}
pub fn thumb(&self, idx: usize) -> Option<&TextureHandle> {
self.thumbs.get(idx).and_then(|t| t.as_ref())
}
pub fn full(&self, idx: usize) -> Option<TextureHandle> {
self.gpu.get(&idx).cloned()
}
pub fn best(&self, idx: usize) -> Option<TextureHandle> {
self.full(idx).or_else(|| self.thumb(idx).cloned())
}
pub fn update(
&mut self,
ctx: &egui::Context,
on_screen: &[usize],
near: &[usize],
rel_path: &dyn Fn(usize) -> Option<String>,
) {
if self.released {
return;
}
let mut got = 0usize;
while let Ok(d) = self.res_rx.try_recv() {
got += 1;
if d.generation == u64::MAX {
self.inflight_thumbs = self.inflight_thumbs.saturating_sub(1);
continue;
}
if d.generation != self.generation {
continue;
}
if d.full {
self.inflight_full.remove(&d.idx);
self.full_ready.push((d.idx, d.image));
} else {
self.inflight_thumbs = self.inflight_thumbs.saturating_sub(1);
self.thumb_ready.push((d.idx, d.image));
}
}
let n = self.thumb_ready.len().min(THUMB_UPLOADS_PER_FRAME);
for (idx, image) in self.thumb_ready.drain(..n) {
if let Some(slot) = self.thumbs.get_mut(idx) {
*slot = Some(ctx.load_texture(
format!("bgthumb/{}/{idx}", self.generation),
image,
TextureOptions::LINEAR,
));
}
}
let on_screen_set: HashSet<usize> = on_screen.iter().copied().collect();
let near_set: HashSet<usize> = near.iter().copied().collect();
for (idx, image) in std::mem::take(&mut self.full_ready) {
if on_screen_set.contains(&idx) || near_set.contains(&idx) {
self.ram.insert(idx, image);
}
}
let mut uploads = 0;
for &idx in on_screen {
if self.gpu.contains_key(&idx) {
continue;
}
if uploads < FULL_UPLOADS_PER_FRAME
&& let Some(image) = self.ram.remove(&idx)
{
self.gpu.insert(
idx,
ctx.load_texture(
format!("bgfull/{}/{idx}", self.generation),
image,
TextureOptions::LINEAR,
),
);
uploads += 1;
}
}
for &idx in on_screen.iter().chain(near.iter()) {
if self.gpu.contains_key(&idx)
|| self.ram.contains_key(&idx)
|| self.inflight_full.contains(&idx)
{
continue;
}
if let Some(rel) = rel_path(idx) {
self.inflight_full.insert(idx);
let _ = self.req_tx.send(Request::Full {
generation: self.generation,
idx,
rel_path: rel,
});
}
}
self.gpu.retain(|idx, _| on_screen_set.contains(idx));
self.ram
.retain(|idx, _| on_screen_set.contains(idx) || near_set.contains(idx));
if got > 0
|| !self.thumb_ready.is_empty()
|| !self.inflight_full.is_empty()
|| self.inflight_thumbs > 0
{
ctx.request_repaint_after(std::time::Duration::from_millis(16));
}
}
}
impl Default for BgCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nothing_is_loaded_while_released_and_loading_resumes_after_restore() {
let ctx = egui::Context::default();
let mut cache = BgCache::new();
cache.reset(1, 3);
let rel = |idx: usize| Some(format!("table{idx}"));
cache.release(None);
assert!(cache.is_released());
cache.update(&ctx, &[0, 1], &[2], &rel);
assert!(
cache.inflight_full.is_empty(),
"a released cache must not ask for pictures"
);
cache.restore();
cache.update(&ctx, &[0, 1], &[2], &rel);
assert_eq!(cache.inflight_full.len(), 3);
}
#[test]
fn release_forgets_what_was_in_flight() {
let mut cache = BgCache::new();
cache.reset(1, 2);
cache.queue_thumb(0, Arc::from(&[][..]));
let before = cache.generation;
cache.release(None);
assert_eq!(cache.inflight_thumbs, 0);
assert!(cache.generation > before);
assert!(cache.thumb(0).is_none());
}
}