perf: cache blurred wallpaper to disk to avoid re-blur on startup
First launch with blur blurs and saves to ~/.cache/moonset/. Subsequent starts load the cached PNG directly (~9x faster). Cache invalidates when wallpaper path, size, mtime, or sigma changes.
This commit is contained in:
parent
622b06da3f
commit
478caed8e0
@ -8,6 +8,7 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
|
||||
### Added
|
||||
|
||||
- Optional background blur via `background_blur` config option (Gaussian blur, `image` crate)
|
||||
- Disk cache for blurred wallpaper (`~/.cache/moonset/`) — avoids re-blurring on subsequent starts
|
||||
|
||||
## [0.1.1] - 2026-03-28
|
||||
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -805,7 +805,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "moonset"
|
||||
version = "0.1.1"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"env_logger",
|
||||
|
||||
172
src/panel.rs
172
src/panel.rs
@ -8,8 +8,12 @@ use gtk4::prelude::*;
|
||||
use gtk4::{self as gtk, gio};
|
||||
use image::imageops;
|
||||
use std::cell::RefCell;
|
||||
use std::path::Path;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use crate::i18n::{load_strings, Strings};
|
||||
use crate::power::{self, PowerError};
|
||||
@ -96,11 +100,118 @@ pub fn load_background_texture(bg_path: &Path, blur_radius: Option<f32>) -> gdk:
|
||||
};
|
||||
|
||||
match blur_radius {
|
||||
Some(sigma) if sigma > 0.0 => apply_blur(&texture, sigma),
|
||||
Some(sigma) if sigma > 0.0 => load_blurred_with_cache(bg_path, &texture, sigma),
|
||||
_ => texture,
|
||||
}
|
||||
}
|
||||
|
||||
// -- Blur cache ----------------------------------------------------------------
|
||||
|
||||
const CACHE_PNG: &str = "blur-cache.png";
|
||||
const CACHE_META: &str = "blur-cache.meta";
|
||||
|
||||
fn blur_cache_dir() -> Option<PathBuf> {
|
||||
dirs::cache_dir().map(|d| d.join("moonset"))
|
||||
}
|
||||
|
||||
/// Build the cache key string for the current wallpaper + sigma.
|
||||
fn build_cache_meta(bg_path: &Path, sigma: f32) -> Option<String> {
|
||||
if bg_path.starts_with("/dev/moonarch/") {
|
||||
let binary = std::env::current_exe().ok()?;
|
||||
let binary_mtime = fs::metadata(&binary)
|
||||
.ok()?
|
||||
.modified()
|
||||
.ok()?
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()?
|
||||
.as_secs();
|
||||
Some(format!(
|
||||
"path={}\nbinary_mtime={}\nsigma={}\n",
|
||||
bg_path.display(), binary_mtime, sigma,
|
||||
))
|
||||
} else {
|
||||
let meta = fs::metadata(bg_path).ok()?;
|
||||
let mtime = meta
|
||||
.modified()
|
||||
.ok()?
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()?
|
||||
.as_secs();
|
||||
Some(format!(
|
||||
"path={}\nsize={}\nmtime={}\nsigma={}\n",
|
||||
bg_path.display(), meta.len(), mtime, sigma,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to load a cached blurred texture if the cache key matches.
|
||||
fn load_cached_blur(cache_dir: &Path, expected_meta: &str) -> Option<gdk::Texture> {
|
||||
let stored_meta = fs::read_to_string(cache_dir.join(CACHE_META)).ok()?;
|
||||
if stored_meta != expected_meta {
|
||||
log::debug!("Blur cache meta mismatch, will re-blur");
|
||||
return None;
|
||||
}
|
||||
let file = gio::File::for_path(cache_dir.join(CACHE_PNG));
|
||||
match gdk::Texture::from_file(&file) {
|
||||
Ok(texture) => {
|
||||
log::debug!("Loaded blurred wallpaper from cache");
|
||||
Some(texture)
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("Failed to load cached blur PNG: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Save a blurred texture to the cache directory.
|
||||
fn save_blur_cache(cache_dir: &Path, texture: &gdk::Texture, meta: &str) {
|
||||
if let Err(e) = save_blur_cache_inner(cache_dir, texture, meta) {
|
||||
log::debug!("Failed to save blur cache: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
fn save_blur_cache_inner(
|
||||
cache_dir: &Path,
|
||||
texture: &gdk::Texture,
|
||||
meta: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fs::create_dir_all(cache_dir)?;
|
||||
|
||||
let png_bytes = texture.save_to_png_bytes();
|
||||
|
||||
let mut f = fs::OpenOptions::new()
|
||||
.create(true).write(true).truncate(true).mode(0o600)
|
||||
.open(cache_dir.join(CACHE_PNG))?;
|
||||
f.write_all(&png_bytes)?;
|
||||
|
||||
// Meta last — incomplete cache is treated as a miss on next start
|
||||
let mut f = fs::OpenOptions::new()
|
||||
.create(true).write(true).truncate(true).mode(0o600)
|
||||
.open(cache_dir.join(CACHE_META))?;
|
||||
f.write_all(meta.as_bytes())?;
|
||||
|
||||
log::debug!("Saved blur cache to {}", cache_dir.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load blurred texture, using disk cache when available.
|
||||
fn load_blurred_with_cache(bg_path: &Path, texture: &gdk::Texture, sigma: f32) -> gdk::Texture {
|
||||
if let Some(cache_dir) = blur_cache_dir() {
|
||||
if let Some(meta) = build_cache_meta(bg_path, sigma) {
|
||||
if let Some(cached) = load_cached_blur(&cache_dir, &meta) {
|
||||
return cached;
|
||||
}
|
||||
let blurred = apply_blur(texture, sigma);
|
||||
save_blur_cache(&cache_dir, &blurred, &meta);
|
||||
return blurred;
|
||||
}
|
||||
}
|
||||
apply_blur(texture, sigma)
|
||||
}
|
||||
|
||||
// -- Blur implementation -------------------------------------------------------
|
||||
|
||||
/// Apply Gaussian blur to a texture and return a blurred texture.
|
||||
fn apply_blur(texture: &gdk::Texture, sigma: f32) -> gdk::Texture {
|
||||
let width = texture.width() as u32;
|
||||
@ -627,4 +738,61 @@ mod tests {
|
||||
let confirm_fn = defs[1].confirm_attr.unwrap();
|
||||
assert_eq!(confirm_fn(strings), "Wirklich abmelden?");
|
||||
}
|
||||
|
||||
// -- Blur cache tests --
|
||||
|
||||
#[test]
|
||||
fn build_cache_meta_for_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = dir.path().join("wallpaper.jpg");
|
||||
fs::write(&file, b"fake image").unwrap();
|
||||
let meta = build_cache_meta(&file, 20.0);
|
||||
assert!(meta.is_some());
|
||||
let meta = meta.unwrap();
|
||||
assert!(meta.contains("path="));
|
||||
assert!(meta.contains("size=10"));
|
||||
assert!(meta.contains("sigma=20"));
|
||||
assert!(meta.contains("mtime="));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_cache_meta_for_gresource() {
|
||||
let path = Path::new("/dev/moonarch/moonset/wallpaper.jpg");
|
||||
let meta = build_cache_meta(path, 15.0);
|
||||
assert!(meta.is_some());
|
||||
let meta = meta.unwrap();
|
||||
assert!(meta.contains("binary_mtime="));
|
||||
assert!(meta.contains("sigma=15"));
|
||||
assert!(!meta.contains("size="));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_cache_meta_missing_file() {
|
||||
let meta = build_cache_meta(Path::new("/nonexistent/wallpaper.jpg"), 20.0);
|
||||
assert!(meta.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_meta_mismatch_returns_none() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
fs::write(
|
||||
dir.path().join(CACHE_META),
|
||||
"path=/old.jpg\nsize=100\nmtime=1\nsigma=20\n",
|
||||
).unwrap();
|
||||
let result = load_cached_blur(
|
||||
dir.path(),
|
||||
"path=/new.jpg\nsize=200\nmtime=2\nsigma=20\n",
|
||||
);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_missing_meta_returns_none() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let result = load_cached_blur(
|
||||
dir.path(),
|
||||
"path=/any.jpg\nsize=1\nmtime=1\nsigma=20\n",
|
||||
);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user