git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commitf46e8b04c07471158a5bea4c880b3e3b9a146a43
parent13976a56d0
authorClaude <noreply@anthropic.com>
date2026-10-04 20:16
perf(text): share cached buffers, cache boxed text; quiet the loop's debug checks

The frame's text path deep-copied every shaped buffer it drew: a cache hit
in get_text_buffer_attrs returned `buffer.clone()`, which copies every line
and glyph, and built a String key to look it up. Boxed text (wrap/align) was
worse: cloned, then re-shaped from scratch every frame.

- The cache holds `Rc<Buffer>`, keyed text → variants so a lookup borrows
  the text; LRU by a counter instead of `Instant::now()` per hit.
- `shared_text_buffer` / `shared_laid_out_buffer` hand out the Rc; the
  frame (`collect_dl_text` → `DlText`), `shaped_cluster_offsets` and
  `ShapingMeasure` use them. The public `get_text_buffer*` functions and
  `TextItem` are unchanged and still return an owned copy.
- Boxed text is cached under its box (wrap width, height, alignment) and
  the scale factor.
- `layout::split_font_string` borrows the family; `parse_font_string`
  wraps it.

Measured in release on a cache hit: a single run 939 ns → 79 ns; a short
boxed label 3.8 µs → 68 ns (longer text costs more before, the same after).

The runner read `CCE_PRESENT_DEBUG` from the environment on every render
and frame callback; it now asks the renderer's OnceLock flag, and the
three pasted timestamp snippets are `debug_clock_ms`. The status-bar app_id
check, which built the whole `settings()` on every pointer motion and press,
is decided once per session (`is_status_bar`).

Also clears the five compiler warnings in the test build: an unused
static in motion.rs (moved into the one non-test fn that uses it), a
duplicated #[test], and three needless `mut`s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwZ8nBQYEej9ic3tVC5v7Z

 src/backend/window_runner.rs | 401 ++++++++++++++++++++++++++++++++-----------
 src/config.rs                |   1 -
 src/layout.rs                |  14 +-
 src/motion.rs                |   8 +-
 src/scene/paint.rs           |   8 +-
 src/vk/mod.rs                |   1 +
 src/vk/renderer.rs           |   2 +-
 src/widget/display/graph.rs  |   2 +-
 src/widget/shaping.rs        |   4 +-
 9 files changed, 328 insertions(+), 113 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 3c4d16f..0d72b4b 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -37,27 +37,129 @@ pub use smithay_client_toolkit::seat::pointer::CursorIcon as PointerCursorIcon;
 use calloop::EventLoop;
 use calloop_wayland_source::WaylandSource;
 use cosmic_text::{FontSystem, Buffer, Attrs, Metrics};
-use crate::widget::{WidgetHost, TextItem, MouseButton, ElementState, MouseScrollDelta, KeyEvent, Key, NamedKey, Position};
+use std::rc::Rc;
+use crate::widget::{WidgetHost, MouseButton, ElementState, MouseScrollDelta, KeyEvent, Key, NamedKey, Position};
 use crate::wayland::detect_scale_factor;
 use crate::vk::{Batch2D, Frame2D, TextSpan, VkRenderer};
 
-#[derive(Hash, PartialEq, Eq, Clone)]
-struct BufferCacheKey {
-    text: String,
+/// One shaped buffer in the text cache, keyed by everything that shapes it
+/// beyond its text (the text is the outer map's key, so a lookup borrows it
+/// rather than allocating).
+struct CachedBuffer {
+    /// Physical font size, in thousandths of a px.
     size_milli: u32,
+    /// The family as the font string names it, size stripped.
     font: Option<String>,
     is_vertical: bool,
     attrs: crate::scene::paint::TextAttrs,
+    /// The scale factor's bits: a laid-out buffer's wrap width and box are
+    /// logical px turned physical by it.
+    scale_bits: u32,
+    /// `Some` for a boxed [`Prim::Text`](crate::scene::paint::Prim::Text) laid
+    /// out by [`get_text_buffer_laid_out`]; `None` for a single run.
+    layout: Option<crate::scene::paint::TextLayout>,
+    /// The laid-out buffer's vertical offset in its box (0 for a single run).
+    voff: f32,
+    /// Shared, not cloned, on a hit: a `Buffer` owns every shaped line and
+    /// glyph, and the frame used to deep-copy one per text prim per frame.
+    buffer: Rc<Buffer>,
+    /// [`BUFFER_TICK`] at the last hit, for least-recently-used eviction.
+    last_used: u64,
 }
 
-#[derive(Clone)]
-struct CachedBuffer {
-    buffer: Buffer,
-    last_accessed: std::time::Instant,
+impl CachedBuffer {
+    fn matches(&self, k: &BufferKey<'_>) -> bool {
+        self.size_milli == k.size_milli
+            && self.font.as_deref() == k.font
+            && self.is_vertical == k.is_vertical
+            && self.attrs == k.attrs
+            && self.scale_bits == k.scale_bits
+            && self.layout == k.layout
+    }
 }
 
+/// A cache lookup's key, borrowed from the caller.
+struct BufferKey<'a> {
+    size_milli: u32,
+    font: Option<&'a str>,
+    is_vertical: bool,
+    attrs: crate::scene::paint::TextAttrs,
+    scale_bits: u32,
+    layout: Option<crate::scene::paint::TextLayout>,
+}
+
+/// The text cache holds at most this many buffers; past it the least
+/// recently used [`BUFFER_EVICT`] go.
+const BUFFER_CAP: usize = 2000;
+const BUFFER_EVICT: usize = 100;
+
 std::thread_local! {
-    static BUFFER_CACHE: std::cell::RefCell<std::collections::HashMap<BufferCacheKey, CachedBuffer>> = std::cell::RefCell::new(std::collections::HashMap::new());
+    /// Text → every shaped variant of it. Variants per text are few (a size,
+    /// a weight, a box), so they are scanned rather than hashed.
+    static BUFFER_CACHE: std::cell::RefCell<std::collections::HashMap<String, Vec<CachedBuffer>>> =
+        std::cell::RefCell::new(std::collections::HashMap::new());
+    static BUFFER_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
+    static BUFFER_TICK: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
+}
+
+fn buffer_tick() -> u64 {
+    BUFFER_TICK.with(|t| {
+        let n = t.get() + 1;
+        t.set(n);
+        n
+    })
+}
+
+/// The cached buffer for `text` under `key`, and its vertical offset.
+fn buffer_cache_get(text: &str, key: &BufferKey<'_>) -> Option<(Rc<Buffer>, f32)> {
+    BUFFER_CACHE.with(|cache| {
+        let mut cache = cache.borrow_mut();
+        let hit = cache.get_mut(text)?.iter_mut().find(|e| e.matches(key))?;
+        hit.last_used = buffer_tick();
+        Some((Rc::clone(&hit.buffer), hit.voff))
+    })
+}
+
+fn buffer_cache_put(text: &str, key: &BufferKey<'_>, buffer: Rc<Buffer>, voff: f32) {
+    BUFFER_CACHE.with(|cache| {
+        let mut cache = cache.borrow_mut();
+        if BUFFER_COUNT.with(|c| c.get()) >= BUFFER_CAP {
+            let mut ticks: Vec<u64> = cache.values().flatten().map(|e| e.last_used).collect();
+            ticks.sort_unstable();
+            let cutoff = ticks[BUFFER_EVICT.min(ticks.len()) - 1];
+            cache.retain(|_, v| {
+                v.retain(|e| e.last_used > cutoff);
+                !v.is_empty()
+            });
+            BUFFER_COUNT.with(|c| c.set(cache.values().map(Vec::len).sum()));
+        }
+        let entry = CachedBuffer {
+            size_milli: key.size_milli,
+            font: key.font.map(str::to_owned),
+            is_vertical: key.is_vertical,
+            attrs: key.attrs,
+            scale_bits: key.scale_bits,
+            layout: key.layout,
+            voff,
+            buffer,
+            last_used: buffer_tick(),
+        };
+        match cache.get_mut(text) {
+            Some(v) => v.push(entry),
+            None => {
+                cache.insert(text.to_owned(), vec![entry]);
+            }
+        }
+        BUFFER_COUNT.with(|c| c.set(c.get() + 1));
+    });
+}
+
+/// The `CCE_PRESENT_DEBUG` traces' timestamp: wall-clock milliseconds, mod
+/// 100 s, short enough to read down a column of lines.
+fn debug_clock_ms() -> u128 {
+    std::time::SystemTime::now()
+        .duration_since(std::time::UNIX_EPOCH)
+        .map_or(0, |d| d.as_millis() % 100_000)
 }
 
 /// A droplet spec resolved against a concrete rect: the push-constant fields
@@ -190,40 +292,40 @@ pub fn get_text_buffer_attrs(
     font: Option<&str>,
     text_attrs: crate::scene::paint::TextAttrs,
 ) -> Buffer {
-    let scale = crate::scale::scale_factor();
-    let mut font_size = size;
-    let mut family_name = None;
+    Buffer::clone(&shared_text_buffer(fs, text, size, font, text_attrs))
+}
 
-    if let Some(font_str) = font {
-        let (parsed_family, parsed_size) = crate::layout::parse_font_string(font_str);
-        if let Some(ps) = parsed_size {
-            font_size = ps;
+/// [`get_text_buffer_attrs`] without the copy: the cached buffer itself,
+/// shared. What the frame and the toolkit's own measuring use — a `Buffer`
+/// owns every shaped line and glyph, so the clone the public functions hand
+/// out costs as much as the text is long.
+pub(crate) fn shared_text_buffer(
+    fs: &mut FontSystem,
+    text: &str,
+    size: f32,
+    font: Option<&str>,
+    text_attrs: crate::scene::paint::TextAttrs,
+) -> Rc<Buffer> {
+    let scale = crate::scale::scale_factor();
+    let (family_name, font_size) = match font {
+        Some(font_str) => {
+            let (family, parsed_size) = crate::layout::split_font_string(font_str);
+            (Some(family), parsed_size.unwrap_or(size))
         }
-        family_name = Some(parsed_family);
-    }
+        None => (None, size),
+    };
 
     let physical_size = font_size * scale;
-    let size_key = (physical_size * 1000.0).round() as u32;
     let is_vertical = crate::IS_VERTICAL.load(std::sync::atomic::Ordering::Relaxed);
-    let key = BufferCacheKey {
-        text: text.to_string(),
-        size_milli: size_key,
-        font: family_name.clone(),
+    let key = BufferKey {
+        size_milli: (physical_size * 1000.0).round() as u32,
+        font: family_name,
         is_vertical,
         attrs: text_attrs,
+        scale_bits: scale.to_bits(),
+        layout: None,
     };
-
-    let cached = BUFFER_CACHE.with(|cache| {
-        let mut cache = cache.borrow_mut();
-        if let Some(cached_item) = cache.get_mut(&key) {
-            cached_item.last_accessed = std::time::Instant::now();
-            Some(cached_item.buffer.clone())
-        } else {
-            None
-        }
-    });
-
-    if let Some(buf) = cached {
+    if let Some((buf, _)) = buffer_cache_get(text, &key) {
         return buf;
     }
 
@@ -238,7 +340,7 @@ pub fn get_text_buffer_attrs(
 
     let (sans_fallback, serif_fallback, mono_fallback, _) = crate::layout::read_preferred_fonts();
 
-    let resolved_storage = family_name.as_deref().and_then(|font_name| match font_name {
+    let resolved_storage = family_name.and_then(|font_name| match font_name {
         "monospace" if !mono_fallback.is_empty() => find_cased_family(fs, &mono_fallback),
         "sans-serif" if !sans_fallback.is_empty() => find_cased_family(fs, &sans_fallback),
         "serif" if !serif_fallback.is_empty() => find_cased_family(fs, &serif_fallback),
@@ -251,8 +353,8 @@ pub fn get_text_buffer_attrs(
         None
     };
 
-    let family = if let Some(ref font_family) = family_name {
-        match font_family.as_str() {
+    let family = if let Some(font_family) = family_name {
+        match font_family {
             "monospace" => {
                 if !mono_fallback.is_empty() {
                     if let Some(ref cased) = resolved_storage {
@@ -310,24 +412,8 @@ pub fn get_text_buffer_attrs(
     buf.set_text(fs, text, attrs, shaping);
     buf.shape_until_scroll(fs, true);
 
-    BUFFER_CACHE.with(|cache| {
-        let mut cache = cache.borrow_mut();
-        if cache.len() >= 2000 {
-            let mut items: Vec<(BufferCacheKey, std::time::Instant)> = cache
-                .iter()
-                .map(|(k, v)| (k.clone(), v.last_accessed))
-                .collect();
-            items.sort_by_key(|&(_, time)| time);
-            for (k, _) in items.iter().take(100) {
-                cache.remove(k);
-            }
-        }
-        cache.insert(key, CachedBuffer {
-            buffer: buf.clone(),
-            last_accessed: std::time::Instant::now(),
-        });
-    });
-
+    let buf = Rc::new(buf);
+    buffer_cache_put(text, &key, Rc::clone(&buf), 0.0);
     buf
 }
 
@@ -346,7 +432,7 @@ pub fn shaped_cluster_offsets(
     font: Option<&str>,
 ) -> Vec<(usize, f32)> {
     let scale = crate::scale::scale_factor().max(1.0);
-    let buffer = get_text_buffer(fs, text, size, font);
+    let buffer = shared_text_buffer(fs, text, size, font, crate::scene::paint::TextAttrs::default());
     let mut out: Vec<(usize, f32)> = Vec::new();
     let mut total: f32 = 0.0;
     for (start, x, w) in normalized_glyph_starts(&buffer, text) {
@@ -395,11 +481,10 @@ pub(crate) fn normalized_glyph_starts(buffer: &Buffer, text: &str) -> Vec<(usize
 }
 
 /// Shape a boxed [`Prim::Text`] (word-wrap + alignment) and return `(buffer, vertical_offset)`.
-/// Reuses [`get_text_buffer_attrs`] for all the family resolution — that returns a *clone* of the
-/// cached single-run buffer, so re-applying metrics/size/align here does not touch the cache — then
-/// re-lays-it-out: a 1.4 line-height (the placed-text convention), the wrap width, per-line
-/// horizontal alignment, and re-shapes. The vertical offset positions the shaped block inside the
-/// box per `align_v`. Uncached by construction (each box may differ in width/align).
+/// Starts from [`get_text_buffer_attrs`]'s single run for all the family resolution, then
+/// re-lays it out: a 1.4 line-height (the placed-text convention), the wrap width, per-line
+/// horizontal alignment, and re-shapes. The vertical offset positions the shaped block inside
+/// the box per `align_v`. Cached beside the single runs, keyed by the box as well.
 pub fn get_text_buffer_laid_out(
     fs: &mut FontSystem,
     text: &str,
@@ -408,20 +493,47 @@ pub fn get_text_buffer_laid_out(
     text_attrs: crate::scene::paint::TextAttrs,
     layout: crate::scene::paint::TextLayout,
 ) -> (Buffer, f32) {
+    let (buf, voff) = shared_laid_out_buffer(fs, text, size, font, text_attrs, layout);
+    (Buffer::clone(&buf), voff)
+}
+
+/// [`get_text_buffer_laid_out`] without the copy, as [`shared_text_buffer`] is to
+/// [`get_text_buffer_attrs`]. Until 2026-10-04 a boxed text was re-shaped from scratch
+/// every frame it was drawn; the box is part of the key now.
+pub(crate) fn shared_laid_out_buffer(
+    fs: &mut FontSystem,
+    text: &str,
+    size: f32,
+    font: Option<&str>,
+    text_attrs: crate::scene::paint::TextAttrs,
+    layout: crate::scene::paint::TextLayout,
+) -> (Rc<Buffer>, f32) {
     use crate::scene::paint::{AlignH, AlignV};
     let scale = crate::scale::scale_factor();
 
-    // Resolved family + attrs come for free (a cache clone we are free to mutate).
-    let mut buf = get_text_buffer_attrs(fs, text, size, font, text_attrs);
-
-    // The font string may override the size ("family:size") — mirror get_text_buffer_attrs.
-    let mut font_size = size;
-    if let Some(font_str) = font {
-        if let (_, Some(ps)) = crate::layout::parse_font_string(font_str) {
-            font_size = ps;
+    // The font string may override the size ("family:size") — mirror shared_text_buffer.
+    let (family, font_size) = match font {
+        Some(font_str) => {
+            let (family, parsed_size) = crate::layout::split_font_string(font_str);
+            (Some(family), parsed_size.unwrap_or(size))
         }
-    }
+        None => (None, size),
+    };
     let physical_size = font_size * scale;
+    let key = BufferKey {
+        size_milli: (physical_size * 1000.0).round() as u32,
+        font: family,
+        is_vertical: crate::IS_VERTICAL.load(std::sync::atomic::Ordering::Relaxed),
+        attrs: text_attrs,
+        scale_bits: scale.to_bits(),
+        layout: Some(layout),
+    };
+    if let Some(hit) = buffer_cache_get(text, &key) {
+        return hit;
+    }
+
+    // Resolved family + attrs come from the single run; this copy is ours to re-lay-out.
+    let mut buf = Buffer::clone(&shared_text_buffer(fs, text, size, font, text_attrs));
     let line_height = physical_size * 1.4;
     buf.set_metrics(fs, Metrics::new(physical_size, line_height));
     buf.set_size(fs, layout.wrap_width.map(|w| w * scale), Some(layout.box_height * scale));
@@ -444,6 +556,8 @@ pub fn get_text_buffer_laid_out(
         AlignV::Middle => ((layout.box_height - total_h) / 2.0).max(0.0),
         AlignV::Bottom => (layout.box_height - total_h).max(0.0),
     };
+    let buf = Rc::new(buf);
+    buffer_cache_put(text, &key, Rc::clone(&buf), voff);
     (buf, voff)
 }
 
@@ -459,11 +573,23 @@ pub struct TextBounds {
     pub bottom: i32,
 }
 
+/// A display-list text prim ready for the glyph pass: a [`TextItem`](crate::widget::TextItem) whose
+/// buffer is the cache's own, shared rather than copied.
+pub(crate) struct DlText {
+    pub(crate) buffer: Rc<Buffer>,
+    pub(crate) x: f32,
+    pub(crate) y: f32,
+    pub(crate) color: cosmic_text::Color,
+    pub(crate) bounds: Option<[f32; 4]>,
+    pub(crate) clip_circle: Option<[f32; 3]>,
+    pub(crate) clip_rrect: Option<[f32; 5]>,
+}
+
 /// The display list's Text prims, shaped through the shared buffer cache and
 /// held for the glyph pass (the [`TextSpan`]s built by [`dl_text_spans`] borrow
 /// these). Clip = the paint walk's item clip ∩ the prim's own bounds, in
 /// logical space. Shared by the window's frame and the context-menu popup's.
-pub(crate) fn collect_dl_text(fs: &mut FontSystem, dl: &crate::scene::paint::DisplayList, out: &mut Vec<TextItem>) {
+pub(crate) fn collect_dl_text(fs: &mut FontSystem, dl: &crate::scene::paint::DisplayList, out: &mut Vec<DlText>) {
     for item in &dl.items {
         if let crate::scene::paint::Prim::Text { text, x, y, font_size, color, alpha, font, bounds, attrs, layout } = &item.prim {
             let clip = item.clip.map(|c| [c.x, c.y, c.x + c.width, c.y + c.height]);
@@ -472,13 +598,13 @@ pub(crate) fn collect_dl_text(fs: &mut FontSystem, dl: &crate::scene::paint::Dis
                 (Some(a), None) => Some(a),
                 (None, b) => b,
             };
-            // Boxed text (wrap/align) shapes uncached and shifts down by the vertical
-            // offset; ordinary labels take the shared cached buffer.
+            // Boxed text (wrap/align) is laid out in its box and shifts down by the
+            // vertical offset; ordinary labels are a single run. Both cached.
             let (buffer, y_off) = match layout {
-                Some(l) => get_text_buffer_laid_out(fs, text, *font_size, font.as_deref(), *attrs, *l),
-                None => (get_text_buffer_attrs(fs, text, *font_size, font.as_deref(), *attrs), 0.0),
+                Some(l) => shared_laid_out_buffer(fs, text, *font_size, font.as_deref(), *attrs, *l),
+                None => (shared_text_buffer(fs, text, *font_size, font.as_deref(), *attrs), 0.0),
             };
-            out.push(TextItem {
+            out.push(DlText {
                 buffer,
                 x: *x,
                 y: *y + y_off,
@@ -499,7 +625,7 @@ pub(crate) fn collect_dl_text(fs: &mut FontSystem, dl: &crate::scene::paint::Dis
 /// The glyph pass's spans for `items`: each clamped to the surface and its
 /// own bounds, then by the popover-occlusion clamp against `overlays`.
 pub(crate) fn dl_text_spans<'a>(
-    items: &'a [TextItem],
+    items: &'a [DlText],
     scale_f32: f32,
     bounds: TextBounds,
     overlays: &[(f32, f32, f32, f32)],
@@ -595,7 +721,7 @@ pub(crate) fn dl_batches_2d(dl_batches: &[DlBatch], scale_f32: f32) -> Vec<Batch
 /// of, and showed as a plate with no legible entries.
 fn popover_occlusion_clamp(
     overlay_rects: &[(f32, f32, f32, f32)],
-    ti: &TextItem,
+    ti: &DlText,
     scale_f32: f32,
     item_bounds: &mut TextBounds,
 ) {
@@ -4119,7 +4245,13 @@ pub struct EngineState<A: Application> {
     /// This frame's display-list text, shaped and held here so the `TextSpan`s built
     /// in the render pass can borrow the buffers (Phase 6 —
     /// [`Application::display_list_text`]).
-    pub dl_text_items: Vec<TextItem>,
+    pub(crate) dl_text_items: Vec<DlText>,
+
+    /// The app is the status bar (`app_id` `cce-status…`): no CSD move,
+    /// resize or resize cursors. Read from `settings()` once per session —
+    /// the checks it serves run on every pointer motion and press, and
+    /// `settings()` builds two `String`s each call.
+    pub(crate) is_status_bar: bool,
 
     /// Drag-and-drop destination state (see [`crate::backend::dnd`]). The
     /// manager is absent when the compositor exposes no wl_data_device_manager;
@@ -4342,7 +4474,7 @@ impl<A: Application> EngineState<A> {
         if let Some(icon) = inner.cursor_icon(lx, ly) {
             return icon;
         }
-        if inner.settings().app_id.starts_with("cce-status")
+        if self.is_status_bar
             || !inner.standard_csd()
             || !inner.csd_resize_borders()
         {
@@ -4628,8 +4760,8 @@ impl<A: Application> EngineState<A> {
             let _callback = surface.frame(&self.qh, ());
             self.frame_callback_pending = true;
             self.frame_callback_armed_at = Some(std::time::Instant::now());
-            if std::env::var("CCE_PRESENT_DEBUG").is_ok() {
-                let t = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() % 100000;
+            if crate::vk::present_debug() {
+                let t = debug_clock_ms();
                 eprintln!("[vk] t={} armed frame callback", t);
             }
         }
@@ -5084,7 +5216,7 @@ impl<A: Application> PointerHandler for EngineState<A> {
                     };
 
                     // Client-Side Decorations (CSD) Drag & Resize Handling
-                    let is_status_bar = self.inner.as_ref().unwrap().settings().app_id.starts_with("cce-status");
+                    let is_status_bar = self.is_status_bar;
                     // Never on the menu popup: its presses are the menu's, and
                     // its coordinates, translated into the window's, would
                     // otherwise read as a resize border or a movable plate.
@@ -5713,8 +5845,8 @@ impl<A: Application> wayland_client::Dispatch<wl_callback::WlCallback, ()> for E
     ) {
         if let wl_callback::Event::Done { .. } = event {
             state.frame_callback_pending = false;
-            if std::env::var("CCE_PRESENT_DEBUG").is_ok() {
-                let t = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() % 100000;
+            if crate::vk::present_debug() {
+                let t = debug_clock_ms();
                 let waited = state.frame_callback_armed_at.map(|a| a.elapsed().as_millis()).unwrap_or(0);
                 eprintln!("[vk] t={} frame-done (waited {}ms)", t, waited);
             }
@@ -6188,6 +6320,7 @@ fn run_session<'l, A: Application>(
         touch_offset: (0.0, 0.0),
         touch_scroll_at: None,
         dl_text_items: Vec::new(),
+        is_status_bar: false,
     };
 
     if let Err(e) = event_queue.roundtrip(&mut engine_state) {
@@ -6209,6 +6342,7 @@ fn run_session<'l, A: Application>(
     };
     let settings = inner.settings();
     crate::scale::set_app_id(settings.app_id.clone());
+    engine_state.is_status_bar = settings.app_id.starts_with("cce-status");
     engine_state.logical_width = settings.width as f32;
     engine_state.logical_height = settings.height as f32;
     engine_state.inner = Some(inner);
@@ -6233,7 +6367,7 @@ fn run_session<'l, A: Application>(
     surface.set_buffer_scale(buffer_scale);
     engine_state.committed_buffer_scale = buffer_scale;
 
-    if settings.app_id.starts_with("cce-status") {
+    if engine_state.is_status_bar {
         let compositor = engine_state.compositor_state.wl_compositor();
         let region = compositor.create_region(&qh, ());
         region.add(0, 0, settings.width as i32, settings.height as i32);
@@ -6345,8 +6479,7 @@ fn run_session<'l, A: Application>(
     /// in the per-iteration path, so a `std::env::var` call here would be I/O
     /// on the loop that is under measurement.
     fn loop_debug() -> bool {
-        static FLAG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
-        *FLAG.get_or_init(|| std::env::var_os("CCE_PRESENT_DEBUG").is_some())
+        crate::vk::present_debug()
     }
 
     /// Loop cadence while something is in motion: one tick per frame.
@@ -6412,11 +6545,7 @@ fn run_session<'l, A: Application>(
             break;
         }
         if let Some(start) = iter_start {
-            let t = std::time::SystemTime::now()
-                .duration_since(std::time::UNIX_EPOCH)
-                .unwrap()
-                .as_millis()
-                % 100000;
+            let t = debug_clock_ms();
             eprintln!(
                 "[vk] t={} loop dispatch={}us pending_cb={}",
                 t,
@@ -6621,8 +6750,8 @@ fn run_session<'l, A: Application>(
                 .is_none_or(|t| t.elapsed().as_millis() > 250)
         {
             engine_state.frame_callback_pending = false;
-            if std::env::var("CCE_PRESENT_DEBUG").is_ok() {
-                let t = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() % 100000;
+            if crate::vk::present_debug() {
+                let t = debug_clock_ms();
                 eprintln!("[vk] t={} starvation fallback fired (callback never came)", t);
             }
         }
@@ -6871,3 +7000,83 @@ mod reconnect_tests {
         );
     }
 }
+
+#[cfg(test)]
+mod text_cache_tests {
+    use super::*;
+    use crate::scene::paint::{AlignH, AlignV, TextAttrs, TextLayout};
+
+    fn boxed(wrap: f32) -> TextLayout {
+        TextLayout { wrap_width: Some(wrap), box_height: 80.0, align_h: AlignH::Center, align_v: AlignV::Middle }
+    }
+
+    /// A hit hands back the cached buffer itself: the frame used to deep-copy
+    /// every text prim's shaped buffer, every frame.
+    #[test]
+    fn a_hit_shares_the_buffer_rather_than_copying_it() {
+        let mut fs = crate::geometry_font_system().lock().unwrap();
+        let attrs = TextAttrs::default();
+        let a = shared_text_buffer(&mut fs, "shared run", 14.0, Some("monospace"), attrs);
+        let b = shared_text_buffer(&mut fs, "shared run", 14.0, Some("monospace"), attrs);
+        assert!(Rc::ptr_eq(&a, &b));
+        let bold = shared_text_buffer(&mut fs, "shared run", 14.0, Some("monospace"), TextAttrs { weight: Some(700), ..attrs });
+        assert!(!Rc::ptr_eq(&a, &bold), "attrs are part of the key");
+        let sized = shared_text_buffer(&mut fs, "shared run", 14.0, Some("monospace 18"), attrs);
+        assert!(!Rc::ptr_eq(&a, &sized), "a size in the font string is part of the key");
+    }
+
+    /// Boxed text is cached by its box, and a hit is what a fresh layout of
+    /// the same box would be.
+    #[test]
+    fn a_laid_out_buffer_is_cached_by_its_box() {
+        let mut fs = crate::geometry_font_system().lock().unwrap();
+        let text = "a line long enough to wrap inside a narrow box";
+        let attrs = TextAttrs::default();
+        let (a, va) = shared_laid_out_buffer(&mut fs, text, 14.0, None, attrs, boxed(90.0));
+        let (b, vb) = shared_laid_out_buffer(&mut fs, text, 14.0, None, attrs, boxed(90.0));
+        assert!(Rc::ptr_eq(&a, &b));
+        assert_eq!(va, vb);
+        let (wide, _) = shared_laid_out_buffer(&mut fs, text, 14.0, None, attrs, boxed(400.0));
+        assert!(!Rc::ptr_eq(&a, &wide), "a different box is a different layout");
+        let single = shared_text_buffer(&mut fs, text, 14.0, None, attrs);
+        assert!(!Rc::ptr_eq(&a, &single), "a box never answers for the single run");
+
+        // The cached layout against one shaped from nothing.
+        BUFFER_CACHE.with(|c| c.borrow_mut().clear());
+        BUFFER_COUNT.with(|c| c.set(0));
+        let (fresh, vf) = shared_laid_out_buffer(&mut fs, text, 14.0, None, attrs, boxed(90.0));
+        assert!(!Rc::ptr_eq(&a, &fresh));
+        assert_eq!(va, vf);
+        let runs = |b: &Buffer| b.layout_runs().map(|r| (r.line_y, r.line_w, r.glyphs.len())).collect::<Vec<_>>();
+        assert_eq!(runs(&a), runs(&fresh));
+    }
+
+    /// The cache holds at most `BUFFER_CAP` buffers, and eviction takes the
+    /// least recently used, not the oldest inserted.
+    #[test]
+    fn eviction_keeps_the_cap_and_the_recently_used() {
+        BUFFER_CACHE.with(|c| c.borrow_mut().clear());
+        BUFFER_COUNT.with(|c| c.set(0));
+        let key = |size_milli| BufferKey {
+            size_milli,
+            font: None,
+            is_vertical: false,
+            attrs: TextAttrs::default(),
+            scale_bits: 1.0f32.to_bits(),
+            layout: None,
+        };
+        let empty = || Rc::new(Buffer::new_empty(Metrics::new(10.0, 10.0)));
+        for i in 0..BUFFER_CAP as u32 {
+            buffer_cache_put(&format!("t{i}"), &key(i), empty(), 0.0);
+        }
+        // The first inserted is touched, so it is no longer the least recent.
+        assert!(buffer_cache_get("t0", &key(0)).is_some());
+        buffer_cache_put("over", &key(0), empty(), 0.0);
+        let count = BUFFER_CACHE.with(|c| c.borrow().values().map(Vec::len).sum::<usize>());
+        assert_eq!(count, BUFFER_CAP - BUFFER_EVICT + 1);
+        assert_eq!(BUFFER_COUNT.with(|c| c.get()), count);
+        assert!(buffer_cache_get("t0", &key(0)).is_some(), "recently used survives");
+        assert!(buffer_cache_get("t1", &key(1)).is_none(), "least recently used goes");
+        assert!(buffer_cache_get("over", &key(0)).is_some());
+    }
+}
diff --git a/src/config.rs b/src/config.rs
index d8b14ec..6d26959 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1296,7 +1296,6 @@ mod tests {
         assert_eq!(crate::color::root_plate_corner_radius(), 17.0, "legacy spelling is not read");
     }
 
-    #[test]
     /// A string list (`rounded_apps "a" "b"`) must survive the JSON round
     /// trip cce-data-editor saves through — it used to come back as ONE arg,
     /// `rounded_apps "a b"`, and the compositor's allowlist then matched
diff --git a/src/layout.rs b/src/layout.rs
index 2da665e..93b11ce 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -448,15 +448,21 @@ pub fn read_config_value(target_key: &str) -> Option<String> {
 }
 
 pub fn parse_font_string(s: &str) -> (String, Option<f32>) {
+    let (family, size) = split_font_string(s);
+    (family.to_string(), size)
+}
+
+/// [`parse_font_string`] borrowing the family from `s` instead of copying it.
+pub fn split_font_string(s: &str) -> (&str, Option<f32>) {
     let s = s.trim();
     if let Some(last_space_idx) = s.rfind(' ') {
         let (family, size_str) = s.split_at(last_space_idx);
         let size_str = size_str.trim();
         if let Ok(size) = size_str.parse::<f32>() {
-            return (family.trim().to_string(), Some(size));
+            return (family.trim(), Some(size));
         }
     }
-    (s.to_string(), None)
+    (s, None)
 }
 
 static SECTION_PADDING: RwLock<f32> = RwLock::new(8.0);
@@ -6593,7 +6599,7 @@ mod tests {
     fn section_text_and_rows_share_one_inset() {
         let mut pc = ProbeTarget::default();
         let (left, cw) = (100.0f32, 320.0f32);
-        let mut sec: SectionContext<'_, ProbeTarget> =
+        let sec: SectionContext<'_, ProbeTarget> =
             SectionContext::new(&mut pc, left, 50.0, cw, "Probe", false, false);
 
         let cols = sec.row_layout(2, 8.0);
@@ -6657,7 +6663,7 @@ mod tests {
     #[test]
     fn section_ax_is_continuous() {
         let mut pc = ProbeTarget::default();
-        let mut sec: SectionContext<'_, ProbeTarget> =
+        let sec: SectionContext<'_, ProbeTarget> =
             SectionContext::new(&mut pc, 100.0, 50.0, 320.0, "Probe", false, false);
         for off in [0.0f32, 1.0, 11.0, 11.999, 12.0, 13.0, 24.0] {
             assert!(
diff --git a/src/motion.rs b/src/motion.rs
index 7554c36..aedaead 100644
--- a/src/motion.rs
+++ b/src/motion.rs
@@ -18,8 +18,7 @@
 //! half a second and nothing needs restarting or reloading. `CCE_ANIMATIONS`
 //! (`0`/`off` or `1`/`on`) overrides the file for one process, for testing.
 
-use std::sync::Mutex;
-use std::time::{Duration, Instant};
+use std::time::Duration;
 
 /// Where the switch lives. Shared with `cce-power-apply`, the writer.
 pub const STATE_PATH: &str = "/run/cce/animations";
@@ -43,8 +42,6 @@ pub fn read_state() -> Option<bool> {
     parse(&std::fs::read_to_string(STATE_PATH).ok()?)
 }
 
-static CACHE: Mutex<Option<(Instant, bool)>> = Mutex::new(None);
-
 // Under `cfg(test)` the switch is the SUITE's, not the machine's: on,
 // unless a test forces it with [`force_for_test`]. Until 2026-09-28
 // `enabled` read `/run/cce/animations` in the test binary too, so three
@@ -87,6 +84,9 @@ fn enabled_on_this_machine() -> bool {
     if let Some(forced) = *ENV.get_or_init(|| std::env::var("CCE_ANIMATIONS").ok().and_then(|v| parse(&v))) {
         return forced;
     }
+    use std::sync::Mutex;
+    use std::time::Instant;
+    static CACHE: Mutex<Option<(Instant, bool)>> = Mutex::new(None);
     let mut cache = CACHE.lock().unwrap_or_else(|e| e.into_inner());
     let now = Instant::now();
     match *cache {
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index c5ce66a..dce741e 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -933,8 +933,8 @@ pub enum AlignV {
 
 /// Box layout for a [`Prim::Text`]: word-wrap width (`Some` ⇒ multiline wrap; `None` ⇒ single
 /// run) and horizontal/vertical alignment within a box of `box_height`. All lengths are logical.
-/// The backend shapes an uncached buffer (`get_text_buffer_laid_out`) so the wrap/align do not
-/// pollute the shared single-run cache, and applies the vertical offset from the shaped height.
+/// The backend shapes it with `get_text_buffer_laid_out`, cached under the box as well as the
+/// text, and applies the vertical offset from the shaped height.
 #[derive(Clone, Copy, Debug, PartialEq)]
 pub struct TextLayout {
     pub wrap_width: Option<f32>,
@@ -1930,8 +1930,8 @@ impl PaintCtx {
     }
 
     /// Boxed text: word-wrap + horizontal/vertical alignment within a box (a placed text box).
-    /// Unlike [`text_with`](PaintCtx::text_with), the backend shapes this uncached with the box
-    /// layout applied. `x, y` are the box's top-left; the backend applies the vertical offset.
+    /// Unlike [`text_with`](PaintCtx::text_with), the backend shapes this with the box layout
+    /// applied (cached per box). `x, y` are the box's top-left; the backend applies the vertical offset.
     #[allow(clippy::too_many_arguments)]
     pub fn text_boxed(
         &mut self,
diff --git a/src/vk/mod.rs b/src/vk/mod.rs
index 41bd80e..2e16c77 100644
--- a/src/vk/mod.rs
+++ b/src/vk/mod.rs
@@ -55,6 +55,7 @@ pub use image::{
     upload_rgba_mipmapped, ImageQuad, PixelFormat,
 };
 pub use renderer::{Batch2D, Frame2D, PlatePush, VkRenderer, MAX_PLATE_FEATURES};
+pub(crate) use renderer::present_debug;
 pub use rt::{RtCamera, RtEnvironment, RtImage, RtImagePixels, RtMaterial, RtOffscreen, RtTriangle};
 pub use scene::{MeshId, SceneDraw, SceneImage, Vertex3D};
 pub use text::TextSpan;
diff --git a/src/vk/renderer.rs b/src/vk/renderer.rs
index d646c34..fe776ce 100644
--- a/src/vk/renderer.rs
+++ b/src/vk/renderer.rs
@@ -2475,7 +2475,7 @@ impl VkRenderer {
 /// or `present img N...` with no matching completion line is blocked inside
 /// the driver; see the off-viewport freeze notes on the present-mode choice
 /// in `create_swapchain`).
-fn present_debug() -> bool {
+pub(crate) fn present_debug() -> bool {
     static FLAG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
     *FLAG.get_or_init(|| std::env::var_os("CCE_PRESENT_DEBUG").is_some())
 }
diff --git a/src/widget/display/graph.rs b/src/widget/display/graph.rs
index 01b2f7e..f18aa2a 100644
--- a/src/widget/display/graph.rs
+++ b/src/widget/display/graph.rs
@@ -2007,7 +2007,7 @@ mod tests {
 
     #[test]
     fn dual_geometry_views_stay_consistent() {
-        let mut g = two_nodes();
+        let g = two_nodes();
         let ctx = UiContext::new();
 
         // The plain view (designer path) and the rounded view (render_widget path) describe
diff --git a/src/widget/shaping.rs b/src/widget/shaping.rs
index 8b8860d..8658b10 100644
--- a/src/widget/shaping.rs
+++ b/src/widget/shaping.rs
@@ -36,7 +36,7 @@ impl Measure for ShapingMeasure {
         if let Some(w) = self.cache.get(&key) {
             return *w;
         }
-        let buf = crate::backend::window_runner::get_text_buffer_attrs(&mut self.fs, text, size, Some(font), attrs);
+        let buf = crate::backend::window_runner::shared_text_buffer(&mut self.fs, text, size, Some(font), attrs);
         // The glyphs' extent, trailing spaces included — the same measure
         // `offsets` ends on. (A layout run's `line_w` leaves trailing
         // whitespace out, so a width taken from it disagreed with where
@@ -58,7 +58,7 @@ impl ShapingMeasure {
     /// combining mark) takes the cluster's start.
     pub fn offsets(&mut self, text: &str, size: f32, font: &str, attrs: TextAttrs) -> Vec<(usize, f32)> {
         let scale = crate::scale::scale_factor().max(0.01);
-        let buf = crate::backend::window_runner::get_text_buffer_attrs(&mut self.fs, text, size, Some(font), attrs);
+        let buf = crate::backend::window_runner::shared_text_buffer(&mut self.fs, text, size, Some(font), attrs);
         let glyphs = crate::backend::window_runner::normalized_glyph_starts(&buf, text);
         let mut starts: Vec<(usize, f32)> = Vec::with_capacity(glyphs.len() + 1);
         let mut width = 0.0f32;