optimize wallpaper loading and free pixels

This commit is contained in:
Samuel Onoja committed 2026-10-06 20:32:07 +01:00
1 parent a6f83689f8
commit 02ec7e5199
1 file changed
+196 -16
+196 -16
View File
@@ -58,7 +58,9 @@ pub(crate) enum WallpaperMode {
struct WallpaperTexture { struct WallpaperTexture {
texture: GlesTexture, texture: GlesTexture,
id: Id, id: Id,
size: Size<i32, Buffer>,
} }
pub(crate) struct WallpaperState { pub(crate) struct WallpaperState {
config: WallpaperConfig, config: WallpaperConfig,
pending: Option<WallpaperConfig>, pending: Option<WallpaperConfig>,
@@ -67,10 +69,18 @@ pub(crate) struct WallpaperState {
commit: CommitCounter, commit: CommitCounter,
owned: bool, owned: bool,
mode: WallpaperMode, mode: WallpaperMode,
receiver: Option<mpsc::Receiver<Result<Pixels, String>>>, receiver: Option<mpsc::Receiver<Result<Pixels, String>>>,
pixels: Option<Pixels>, pixels: Option<Pixels>,
textures: HashMap<ErasedContextId, WallpaperTexture>, textures: HashMap<ErasedContextId, WallpaperTexture>,
upload_retries: HashMap<ErasedContextId, Instant>, upload_retries: HashMap<ErasedContextId, Instant>,
// Prevent a missing/corrupt source from being decoded on every frame when
// a new renderer context needs the wallpaper.
decode_failed: bool,
wakeup: Option<smithay::reexports::calloop::LoopSignal>, wakeup: Option<smithay::reexports::calloop::LoopSignal>,
retry_timer_pending: Rc<Cell<bool>>, retry_timer_pending: Rc<Cell<bool>>,
retry_wakeup: Rc<Cell<bool>>, retry_wakeup: Rc<Cell<bool>>,
@@ -97,12 +107,15 @@ impl WallpaperState {
) -> Self { ) -> Self {
let retained = config.clone(); let retained = config.clone();
let owned = config.path.as_ref().is_some_and(|path| path.is_file()); let owned = config.path.as_ref().is_some_and(|path| path.is_file());
let decode_failed = config.path.is_some() && !owned;
let receiver = if owned { let receiver = if owned {
Some(decoder.submit(config.path.clone().unwrap(), wakeup.clone())) Some(decoder.submit(config.path.clone().unwrap(), wakeup.clone()))
} else { } else {
decoder.cancel_pending(); decoder.cancel_pending();
None None
}; };
Self { Self {
config: retained, config: retained,
pending: None, pending: None,
@@ -115,20 +128,36 @@ impl WallpaperState {
pixels: None, pixels: None,
textures: HashMap::new(), textures: HashMap::new(),
upload_retries: HashMap::new(), upload_retries: HashMap::new(),
decode_failed,
wakeup, wakeup,
retry_timer_pending: Rc::default(), retry_timer_pending: Rc::default(),
retry_wakeup: Rc::default(), retry_wakeup: Rc::default(),
} }
} }
fn ensure_pixels(&mut self) {
if self.pixels.is_some() || self.receiver.is_some() || self.decode_failed {
return;
}
let Some(path) = self.config.path.clone() else {
return;
};
self.receiver = Some(self.decoder.submit(path, self.wakeup.clone()))
}
pub(crate) fn configuration(&self) -> &WallpaperConfig { pub(crate) fn configuration(&self) -> &WallpaperConfig {
&self.config &self.config
} }
pub(crate) fn failed(&self) -> bool { pub(crate) fn failed(&self) -> bool {
self.config.path.is_some() && self.receiver.is_none() && self.pixels.is_none() self.config.path.is_some()
&& self.receiver.is_none()
&& self.pixels.is_none()
&& self.textures.is_empty()
&& self.decode_failed
} }
#[cfg(test)] #[cfg(test)]
pub fn owns_background(&self) -> bool { pub fn owns_background(&self) -> bool {
self.owned self.owned
@@ -173,6 +202,7 @@ impl WallpaperState {
self.pixels = None; self.pixels = None;
self.textures.clear(); self.textures.clear();
self.upload_retries.clear(); self.upload_retries.clear();
self.decode_failed = false;
self.commit.increment(); self.commit.increment();
} }
} }
@@ -229,10 +259,14 @@ impl WallpaperState {
let Some(receiver) = &self.receiver else { let Some(receiver) = &self.receiver else {
return false; return false;
}; };
match receiver.try_recv() { match receiver.try_recv() {
Ok(result) => { Ok(result) => {
self.receiver = None; self.receiver = None;
let loading = self.loading.take(); let loading = self.loading.take();
let replacing_wallpaper = loading.is_some();
match result { match result {
Ok(pixels) => { Ok(pixels) => {
if let Some(config) = loading { if let Some(config) = loading {
@@ -247,18 +281,36 @@ impl WallpaperState {
bytes = pixels.as_raw().len(), bytes = pixels.as_raw().len(),
"decoded compositor wallpaper once" "decoded compositor wallpaper once"
); );
self.pixels = Some(pixels); self.pixels = Some(pixels);
self.textures.clear(); self.decode_failed = false;
// A decode for a new wallpaper invalidates textures from
// the old image. A lazy re-decode for another context does
// not: those existing textures are still valid.
if replacing_wallpaper {
self.textures.clear();
}
self.upload_retries.clear(); self.upload_retries.clear();
self.commit.increment(); self.commit.increment();
} }
Err(error) => { Err(error) => {
// A failed replacement does not poison the currently
// accepted wallpaper. Only failure to decode the accepted
// source itself should suppress repeated lazy re-decodes.
if !replacing_wallpaper {
self.decode_failed = true;
}
tracing::warn!(%error, "wallpaper decode failed; retaining previous image") tracing::warn!(%error, "wallpaper decode failed; retaining previous image")
} }
} }
if let Some(pending) = self.pending.take() { if let Some(pending) = self.pending.take() {
self.reload(pending); self.reload(pending);
} }
true true
} }
Err(mpsc::TryRecvError::Disconnected) => { Err(mpsc::TryRecvError::Disconnected) => {
@@ -271,36 +323,68 @@ impl WallpaperState {
} }
pub fn element(&mut self, renderer: &mut GlesRenderer, output: &Output) -> Option<NativeTextureElement> { pub fn element(&mut self, renderer: &mut GlesRenderer, output: &Output) -> Option<NativeTextureElement> {
let pixels = self.pixels.as_ref()?;
let context = renderer.context_id().erased(); let context = renderer.context_id().erased();
if !self.textures.contains_key(&context) { if !self.textures.contains_key(&context) {
if !self.upload_ready(&context, Instant::now()) { if !self.upload_ready(&context, Instant::now()) {
return None; return None;
} }
let size = Size::from((pixels.width() as i32, pixels.height() as i32));
match renderer.import_memory(pixels.as_raw(), Fourcc::Abgr8888, size, false) { if self.pixels.is_none() {
self.ensure_pixels();
return None;
}
let (size, bytes, texture) = {
let pixels = self.pixels.as_ref()?;
let size = Size::from((pixels.width() as i32, pixels.height() as i32));
let bytes = pixels.as_raw().len();
let texture = renderer.import_memory(pixels.as_raw(), Fourcc::Abgr8888, size, false);
(size, bytes, texture)
};
match texture {
Ok(texture) => { Ok(texture) => {
self.upload_retries.remove(&context); self.upload_retries.remove(&context);
tracing::debug!(bytes = pixels.as_raw().len(), "uploaded shared wallpaper texture");
self.textures self.textures.insert(
.insert(context.clone(), WallpaperTexture { texture, id: Id::new() }); context.clone(),
WallpaperTexture {
texture,
id: Id::new(),
size,
},
);
// import_memory() has produced a renderer-owned texture.
// The decoded RGBA mapping is only staging and no longer
// needs to stay resident.
self.pixels = None;
tracing::debug!(
bytes,
"uploaded shared wallpaper texture and released CPU staging pixels"
);
} }
Err(error) => { Err(error) => {
// Avoid retrying a large failed allocation every frame, but // Keep pixels alive on failure so the retry does not require
// allow recovery from temporary GPU memory pressure. // another decode.
self.upload_retries.insert(context, Instant::now() + UPLOAD_RETRY_DELAY); self.upload_retries.insert(context, Instant::now() + UPLOAD_RETRY_DELAY);
tracing::debug!(%error, "wallpaper texture import failed"); tracing::debug!(%error, "wallpaper texture import failed");
return None; return None;
} }
} }
} }
let cached = self.textures.get(&context)?; let cached = self.textures.get(&context)?;
let output_size = output.current_transform().transform_size(output.current_mode()?.size); let output_size = output.current_transform().transform_size(output.current_mode()?.size);
let (geometry, source) = image_geometry(
(pixels.width() as i32, pixels.height() as i32).into(), let (geometry, source) = image_geometry(cached.size, output_size, self.mode);
output_size,
self.mode,
);
Some(NativeTextureElement { Some(NativeTextureElement {
id: cached.id.clone(), id: cached.id.clone(),
commit: self.commit, commit: self.commit,
@@ -571,4 +655,100 @@ mod tests {
assert_eq!(image::image_dimensions(&path).unwrap(), (3840, 2160)); assert_eq!(image::image_dimensions(&path).unwrap(), (3840, 2160));
assert!(WallpaperState::new(WallpaperConfig::default()).owns_background()); assert!(WallpaperState::new(WallpaperConfig::default()).owns_background());
} }
#[test]
fn released_staging_is_not_a_failed_wallpaper() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("wallpaper.png");
image::RgbaImage::from_pixel(2, 2, image::Rgba([32, 80, 160, 255]))
.save(&path)
.unwrap();
let mut state = WallpaperState::new(WallpaperConfig {
path: Some(path),
mode: WallpaperMode::Fill,
});
let deadline = Instant::now() + Duration::from_secs(2);
while !state.poll() {
assert!(Instant::now() < deadline);
std::thread::sleep(Duration::from_millis(1));
}
assert!(state.pixels.is_some());
assert!(!state.failed());
// Model successful upload releasing CPU staging.
state.pixels = None;
// Missing staging is normal after upload, not a decode failure.
assert!(!state.failed());
}
#[test]
fn missing_staging_is_lazily_redecoded() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("wallpaper.png");
image::RgbaImage::from_pixel(3, 2, image::Rgba([32, 80, 160, 255]))
.save(&path)
.unwrap();
let mut state = WallpaperState::new(WallpaperConfig {
path: Some(path),
mode: WallpaperMode::Fill,
});
let deadline = Instant::now() + Duration::from_secs(2);
while !state.poll() {
assert!(Instant::now() < deadline);
std::thread::sleep(Duration::from_millis(1));
}
state.pixels = None;
assert!(state.receiver.is_none());
state.ensure_pixels();
assert!(state.receiver.is_some());
let deadline = Instant::now() + Duration::from_secs(2);
while !state.poll() {
assert!(Instant::now() < deadline);
std::thread::sleep(Duration::from_millis(1));
}
assert_eq!(state.pixels.as_ref().unwrap().dimensions(), (3, 2));
}
#[test]
fn failed_lazy_decode_is_not_restarted_every_frame() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("wallpaper");
std::fs::write(&path, b"not an image").unwrap();
let mut state = WallpaperState::new(WallpaperConfig {
path: Some(path),
mode: WallpaperMode::Fill,
});
let deadline = Instant::now() + Duration::from_secs(2);
while !state.poll() {
assert!(Instant::now() < deadline);
std::thread::sleep(Duration::from_millis(1));
}
assert!(state.decode_failed);
assert!(state.failed());
state.ensure_pixels();
assert!(
state.receiver.is_none(),
"failed source must not restart a decode every frame"
);
}
} }