mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-29 12:37:51 -06:00
Renders run in dedicated module Web Workers, each a second instance of the app's wasm module (index.html compiles it once and posts the WebAssembly.Module to worker.js). A job crosses as JSON (WireJob: source, settings, request); the worker reads originals and cached thumbnails from browser storage itself, keeps decoded sources and per-view stage caches, and transfers RGBA back. Jobs follow the worker that decoded the photo; the loupe waits for it. No wasm threads or SharedArrayBuffer needed; falls back to inline rendering if no worker starts (?workers=0). Thumbnail disk cache on the web: thumbs/<key>.jpg in OPFS/IndexedDB with an index on the main thread, pruned to the desktop budget (2 GB → 80 %, LRU). ui-egui: RenderOffload hook in the Renderer (queue/priorities/dedup stay in the UI). engine: RenderJob.origin + MediaCache::origin_ref; preview: encode_jpeg/decode_jpeg public. xtask web: copies worker.js; --serve sends COOP/COEP/CORP (documented in docs/web.md). Headless Chrome, cold load + grid scroll, 24 demo photos: main-thread busy 12.4 s → 0.7–1.1 s, long tasks 23 (max 1.1 s) → 0; grid filled in 6.3–8.4 s vs 13–16 s; slider job 4 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
87 lines
2.9 KiB
Rust
87 lines
2.9 KiB
Rust
//! Previews: everything that makes a big library show up instantly.
|
|
//!
|
|
//! - [`hash`]: 128-bit content hashes (file bytes → duplicate detection and cache keys).
|
|
//! - [`Lru`]: a cost-bounded memory LRU (decoded sources, rendered thumbnails).
|
|
//! - [`DiskCache`]: rendered thumbnails as JPEG files keyed by `(content hash, settings hash,
|
|
//! size, renderer version)`, bounded in bytes (least recently used files are pruned).
|
|
//! - [`PreviewCache`]: memory LRU in front of the disk cache; shared by render workers.
|
|
//! - [`JobPool`]: worker threads with per-slot de-duplication and priorities (visible thumbnails
|
|
//! before off-screen prefetch); runs jobs inline on wasm.
|
|
//!
|
|
//! No UI dependencies (L3): the egui frontend, the CLI and the MCP server share it.
|
|
#![forbid(unsafe_code)]
|
|
|
|
pub mod disk;
|
|
pub mod hash;
|
|
pub mod lru;
|
|
pub mod pool;
|
|
|
|
use std::path::Path;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
pub use disk::{DiskCache, decode_jpeg, encode_jpeg};
|
|
pub use hash::{Hash128, Hasher128, hash_bytes};
|
|
use lightcraft_raster::Rgba8;
|
|
pub use lru::Lru;
|
|
pub use pool::JobPool;
|
|
|
|
/// Rendered-thumbnail cache: memory LRU over an optional disk cache.
|
|
pub struct PreviewCache {
|
|
mem: Mutex<Lru<Hash128, Arc<Rgba8>>>,
|
|
disk: Option<DiskCache>,
|
|
}
|
|
|
|
impl PreviewCache {
|
|
/// Memory-only cache of at most `mem_bytes`.
|
|
pub fn memory(mem_bytes: usize) -> PreviewCache {
|
|
PreviewCache { mem: Mutex::new(Lru::new(mem_bytes)), disk: None }
|
|
}
|
|
|
|
/// Memory LRU + disk cache in `dir` (at most `disk_bytes`).
|
|
pub fn with_disk(mem_bytes: usize, dir: &Path, disk_bytes: u64) -> PreviewCache {
|
|
PreviewCache { mem: Mutex::new(Lru::new(mem_bytes)), disk: Some(DiskCache::new(dir, disk_bytes)) }
|
|
}
|
|
|
|
pub fn disk(&self) -> Option<&DiskCache> {
|
|
self.disk.as_ref()
|
|
}
|
|
|
|
pub fn get(&self, key: Hash128) -> Option<Arc<Rgba8>> {
|
|
if let Some(v) = self.mem.lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
|
|
return Some(v.clone());
|
|
}
|
|
let img = Arc::new(self.disk.as_ref()?.get(key)?);
|
|
self.mem.lock().unwrap_or_else(|e| e.into_inner()).insert(key, img.clone(), cost(&img));
|
|
Some(img)
|
|
}
|
|
|
|
pub fn put(&self, key: Hash128, img: Arc<Rgba8>) {
|
|
if let Some(d) = &self.disk {
|
|
d.put(key, &img);
|
|
}
|
|
let c = cost(&img);
|
|
self.mem.lock().unwrap_or_else(|e| e.into_inner()).insert(key, img, c);
|
|
}
|
|
|
|
/// Drop everything (memory and disk).
|
|
pub fn clear(&self) {
|
|
self.mem.lock().unwrap_or_else(|e| e.into_inner()).clear();
|
|
if let Some(d) = &self.disk {
|
|
d.clear();
|
|
}
|
|
}
|
|
|
|
/// (entries, bytes) held in memory.
|
|
pub fn mem_usage(&self) -> (usize, usize) {
|
|
let m = self.mem.lock().unwrap_or_else(|e| e.into_inner());
|
|
(m.len(), m.cost())
|
|
}
|
|
}
|
|
|
|
fn cost(img: &Rgba8) -> usize {
|
|
img.width * img.height * 4 + 64
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|