From e917c5393fa6d18c4e6debfd346ff9d4813c800f Mon Sep 17 00:00:00 2001 From: Mollusk Date: Sun, 14 Jun 2026 17:13:00 -0400 Subject: [PATCH] fix(avatars): cache image handles so avatars don't flicker on redraw MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iced's `image::Handle::from_bytes` assigns a fresh *unique* id on every call (unlike `from_path`, which hashes). `avatar_view` built the handle inline in `view()`, so every repaint produced a "new" image and iced re-uploaded the texture each frame. Any redraw triggered it — notably the redraws fired on mouse movement — so visible avatars flickered constantly while the mouse moved. Fix: cache handles by a content hash of the PNG bytes (thread-local, UI thread) and reuse the same `Handle` across redraws, giving a stable texture id. Covers both presets and custom uploads. Build + clippy clean. Co-Authored-By: Claude Opus 4.8 --- src/app/mod.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 4fdd3cf..5d6e03e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2915,6 +2915,33 @@ fn avatar_badge<'a>(name: &str, key: &str, size: f32) -> Element<'a, AppMessage> .into() } +thread_local! { + /// Cache of avatar image handles, keyed by a hash of the PNG bytes, so the + /// SAME `image::Handle` (and thus the same GPU texture id) is reused across + /// redraws. `image::Handle::from_bytes` mints a fresh *unique* id on every + /// call, so building handles inline in `view()` made iced re-upload the + /// texture on every repaint — including the redraws fired on each mouse move — + /// which showed up as constant flicker. Lives on the (single) UI thread. + static AVATAR_HANDLE_CACHE: std::cell::RefCell> = + std::cell::RefCell::new(HashMap::new()); +} + +/// A stable image handle for these exact PNG bytes (cached by content hash), so +/// it keeps the same id across redraws — see [`AVATAR_HANDLE_CACHE`]. +fn cached_image_handle(bytes: bytes::Bytes) -> iced::widget::image::Handle { + use std::hash::{Hash, Hasher}; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + bytes.as_ref().hash(&mut hasher); + let key = hasher.finish(); + AVATAR_HANDLE_CACHE.with(|cache| { + cache + .borrow_mut() + .entry(key) + .or_insert_with(|| iced::widget::image::Handle::from_bytes(bytes)) + .clone() + }) +} + /// Render a participant's avatar (W4): the chosen preset image if any, else the /// monogram fallback. `name`/`key` feed the monogram; `size` is the diameter. fn avatar_view<'a>( @@ -2928,7 +2955,7 @@ fn avatar_view<'a>( .map(bytes::Bytes::from_static) .or_else(|| avatar.custom_png().map(bytes::Bytes::from)); match png_bytes { - Some(b) => iced::widget::image(iced::widget::image::Handle::from_bytes(b)) + Some(b) => iced::widget::image(cached_image_handle(b)) .width(iced::Length::Fixed(size)) .height(iced::Length::Fixed(size)) .into(),