git.lucas.co / cce-status-interface
status bar
git clone https://git.lucas.co/cce-status-interface.git

commit73ab926f2e866c98eb3b20ef9dd117ff315485d9
parent87dff6f43d
authorLucas Galante <[email protected]>
date2026-09-19 00:23
fix(icons): the glyph cache belongs to the renderer, not the process

A lost Wayland transport is repaired by opening a new session around the
same Application, which rebuilds the renderer and its image table — so
every id `tinted_icon` had cached named an image that no longer existed.
Draws for an unknown id are skipped rather than reported, which is why a
reconnected bar came back with its numbers and not one glyph, and stayed
that way until the process was restarted. The stats segment reconnected
at 22:53 last night and lost all five.

`renderer_init` now drops the cache on every renderer after the first
(the first is the one `new()`'s uploads are queued for) and asks for a
rebuild, which re-uploads into the renderer just created.

Verified in a shadow session with CCE_UI_FAULT_RECONNECT: before the
fix, the injected drop left the numbers alone in the bubble; after it,
the row comes back whole.

Co-Authored-By: Claude Opus 5 <[email protected]>

 CLAUDE.md    |  9 ++++++++-
 src/icons.rs | 30 ++++++++++++++++++++++++++++--
 src/main.rs  | 20 ++++++++++++++++++++
 3 files changed, 56 insertions(+), 3 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 46d3479..7a21e7e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -118,7 +118,14 @@ The glyphs come from the **cce-icons** crate via `cce_ui::icons_dir()`
 `cce_ui::upload_icon`: a `Prim::Image` has alpha and no color, and the
 artwork is white, so `icons.rs::tinted_icon` rasterizes the SVG itself
 (`cce_ui::rasterize_svg`), multiplies it by the readout's raw-sRGB color and
-uploads it, cached per `(name, px, color)` for the life of the process. A
+uploads it, cached per `(name, px, color)` — for the life of the RENDERER,
+not the process: the cache holds renderer image ids, and a reconnect
+(cce-ui repairs a lost transport by opening a new session around the same
+`Application`) rebuilds the renderer and its image table, leaving every
+cached id naming nothing. A draw for an unknown id is skipped rather than
+reported, so a reconnected bar came back with its numbers and no glyphs at
+all; `renderer_init` now calls `icons::drop_textures()` on every renderer
+after the first, and the rebuild it forces re-uploads them. A
 glyph that fails to load falls back to the old text readout ("Cpu 45%"), so
 a bar started without the icon set is still attributable; **a shadow session
 needs `CCE_ICONS_DIR` exported into the spawn**, its HOME being elsewhere,
diff --git a/src/icons.rs b/src/icons.rs
index 39a3c73..13d4af0 100644
--- a/src/icons.rs
+++ b/src/icons.rs
@@ -18,13 +18,16 @@
 use std::collections::HashMap;
 use std::sync::Mutex;
 
+/// `(name, px, tint)` → the uploaded texture, or `None` for a glyph that could
+/// not be loaded (the miss is cached too, so the warning is logged once).
+type Key = (String, u32, [u8; 3]);
+static CACHE: Mutex<Option<HashMap<Key, Option<(u32, u32, u32)>>>> = Mutex::new(None);
+
 /// Rasterize `<name>.svg` from cce-icons at `px` on its longer side, tinted
 /// to `rgb` (raw sRGB, like every text color here — uploaded images are
 /// sampled as sRGB), and upload it as a renderer texture. Returns the image
 /// id plus the pixel size for `PaintCtx::image`.
 pub(crate) fn tinted_icon(name: &str, px: u32, rgb: [u8; 3]) -> Option<(u32, u32, u32)> {
-    type Key = (String, u32, [u8; 3]);
-    static CACHE: Mutex<Option<HashMap<Key, Option<(u32, u32, u32)>>>> = Mutex::new(None);
     let key = (name.to_string(), px, rgb);
     let mut guard = CACHE.lock().unwrap();
     let cache = guard.get_or_insert_with(HashMap::new);
@@ -67,3 +70,26 @@ pub(crate) fn tint_of(color: [f32; 4]) -> [u8; 3] {
         (color[2] * 255.0).round() as u8,
     ]
 }
+
+/// Forget every uploaded glyph, freeing its texture.
+///
+/// The cache holds **renderer** image ids, and a renderer does not outlive its
+/// session: `cce-ui`'s `window_runner` repairs a lost Wayland transport by
+/// opening a new session around the same `Application`, which rebuilds the
+/// renderer and with it the image table. The cached ids then name images that
+/// no longer exist, and a draw for an unknown id is skipped rather than
+/// reported — so a bar that reconnected came back with its numbers and no
+/// glyphs at all, until the process was restarted.
+///
+/// Called from `Application::renderer_init` when the renderer it is handed is a
+/// *replacement*; the first renderer of the process is the one the uploads
+/// queued from `new()` are waiting for, so dropping them there would only
+/// upload, destroy and re-upload the same five glyphs before the first frame.
+pub(crate) fn drop_textures() {
+    let mut guard = CACHE.lock().unwrap();
+    let Some(cache) = guard.as_mut() else { return };
+    for (id, _, _) in cache.values().flatten() {
+        cce_ui::vk::free_image(*id);
+    }
+    cache.clear();
+}
diff --git a/src/main.rs b/src/main.rs
index 5dd3a80..f6a08aa 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -562,6 +562,11 @@ struct StatusApp {
     selected_module_side: Option<Side>,
     status_hide_mode: bool,
     adjust_position_mode: bool,
+    /// Whether a renderer has been handed to this app yet. The first one is
+    /// the one `new()`'s glyph uploads are queued for; every later one is a
+    /// reconnect, and the ids cached in `icons` name images that died with
+    /// the renderer being replaced — see `renderer_init`.
+    seen_renderer: bool,
 }
 
 /// The Wayland `app_id` a segment presents — the compositor places segments
@@ -1534,6 +1539,7 @@ impl cce_ui::engine::Application for StatusApp {
             selected_module_side: selected_module.as_ref().map(|(_, s)| s.clone()),
             status_hide_mode: false,
             adjust_position_mode: false,
+            seen_renderer: false,
         };
 
         app.rebuild_layout();
@@ -1953,6 +1959,20 @@ impl cce_ui::engine::Application for StatusApp {
         Some(pc.finish())
     }
 
+    /// A reconnect is a new session around the SAME app (cce-ui's
+    /// `window_runner` repairs a lost transport rather than restarting the
+    /// process), and the renderer is rebuilt with it — so the glyph textures
+    /// `icons::tinted_icon` cached ids for no longer exist. A draw for an
+    /// unknown image id is skipped silently, which is why a reconnected bar
+    /// kept its numbers and lost every glyph. Drop the cache and rebuild, so
+    /// the next layout uploads into the renderer just created.
+    fn renderer_init(&mut self, _renderer: &mut cce_ui::vk::VkRenderer) {
+        if std::mem::replace(&mut self.seen_renderer, true) {
+            crate::icons::drop_textures();
+            self.needs_rebuild = true;
+        }
+    }
+
     fn display_list_text(&self) -> bool {
         true
     }