optimize wallpaper loading and free pixels
This commit is contained in:
1 parent
a6f83689f8
commit
02ec7e5199
1 file changed
+196
-16
+196
-16
@@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user