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

commitd8bf03bfbbb032b7afbb7dba691111b646dd1215
parent3f3ccd4537
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-05 21:38
perf(runner): the warm-down commits without drawing

For 200 ms after every genuine redraw (each hover change, keystroke,
click) the loop kept "re-rendering the cached frame" so the compositor
would keep servicing this surface's frame callbacks at vsync (37e140e:
sparse commits got theirs 22-128 ms late). Nothing was cached: each
step ran the whole render -- prepare_text sweep, display_list,
tessellation, uploads, the frost passes -- and presented an identical
frame with full-surface damage, which the compositor then re-blurred.
About 12 such frames per interaction, in every client.

A warm-down step is now a frame callback plus a bufferless commit
(keepalive_commit). wlroots schedules an output frame for a commit that
asks for a callback, so the cadence is kept with no damage and no draw.
Its callback carries its own user data (KeepAlive) and flag, so a
genuine redraw never waits on it; an unanswered one (occluded surface)
is abandoned after 250 ms like the starvation fallback.

Measured in a scale-2 shadow, 12 pointer moves 450 ms apart across the
demo app:
  before: 133 full renders, frame-done waits 10-22 ms
  after:   11 full renders + 131 keepalives; genuine waits 15-16 ms,
           keepalive waits 14-20 ms
A screenshot after the sweep shows the window intact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 src/backend/shell.rs         |  9 ++++++-
 src/backend/window_runner.rs | 60 ++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 66 insertions(+), 3 deletions(-)

diff --git a/src/backend/shell.rs b/src/backend/shell.rs
index 78dc71e..c2b7153 100644
--- a/src/backend/shell.rs
+++ b/src/backend/shell.rs
@@ -30,6 +30,12 @@ pub const IDLE_DISPATCH: Duration = Duration::from_millis(1000);
 /// commit), while a continuously committing surface is serviced in one frame
 /// (~16ms). A short warm-down keeps interactive sequences (hover, typing,
 /// scrolling) in the healthy continuous regime; idle still idles.
+///
+/// A warm-down step need not draw: the Wayland shell commits a frame callback
+/// with no buffer (`EngineState::keepalive_commit`). Until 2026-10-05 it
+/// re-rendered the whole frame and presented it with full damage — about 12
+/// identical frames after every hover or keystroke, each re-blurred by the
+/// compositor.
 pub const WARM_DOWN: Duration = Duration::from_millis(200);
 
 /// Upper bound on an idle sleep. The loop is woken early by any event the
@@ -89,7 +95,8 @@ pub trait Shell {
     fn configured(&self) -> bool;
 
     /// Build and present a frame. `fresh` is a frame something asked for;
-    /// otherwise it is a warm-down re-render (see [`WARM_DOWN`]).
+    /// otherwise it is a warm-down step (see [`WARM_DOWN`]): nothing changed,
+    /// so a shell that can keep its pacing without drawing should.
     fn present(&mut self, fresh: bool);
 }
 
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 2372973..2215776 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -142,6 +142,11 @@ pub struct EngineState<A: Application> {
     /// When the pending frame callback was armed — the starvation fallback's
     /// clock (see the render gate in `run`).
     pub frame_callback_armed_at: Option<std::time::Instant>,
+    /// A warm-down commit's frame callback is outstanding (see
+    /// [`EngineState::keepalive_commit`]). Separate from
+    /// `frame_callback_pending` on purpose: a genuine redraw never waits on it.
+    pub keepalive_pending: bool,
+    pub keepalive_armed_at: Option<std::time::Instant>,
     /// Consecutive renders skipped by the extent gate (pending swapchain size
     /// != the size the current logical size and scale call for). Normally 0 or
     /// 1; a persistent count means no frame is presenting and deserves a warn.
@@ -673,8 +678,15 @@ impl<A: Application> Shell for EngineState<A> {
 
     fn present(&mut self, fresh: bool) {
         if !fresh {
-            // A warm-down re-render: the window alone.
-            self.render();
+            // A warm-down step: the window alone, and nothing drawn — the
+            // pixels have not changed. An occluded surface gets no callbacks;
+            // past 250 ms stop waiting for one, as the starvation fallback does
+            // for a real frame.
+            let waiting = self.keepalive_pending
+                && self.keepalive_armed_at.is_some_and(|t| t.elapsed().as_millis() < 250);
+            if !waiting {
+                self.keepalive_commit();
+            }
             return;
         }
         // A menu handed over from the window commits first, so
@@ -690,6 +702,48 @@ impl<A: Application> Shell for EngineState<A> {
     }
 }
 
+impl<A: Application> EngineState<A> {
+    /// One warm-down step: a frame callback and a commit with no buffer, so
+    /// the compositor keeps servicing this surface's callbacks at vsync
+    /// (sparse commits were measured getting theirs 22-128 ms late) while
+    /// nothing is drawn, uploaded or re-composited. wlroots schedules an
+    /// output frame for a commit that asks for a callback, so it arrives
+    /// without any damage.
+    fn keepalive_commit(&mut self) {
+        let Some(ref surface) = self.surface else { return };
+        let _callback = surface.frame(&self.qh, KeepAlive);
+        surface.commit();
+        self.keepalive_pending = true;
+        self.keepalive_armed_at = Some(std::time::Instant::now());
+        if crate::vk::present_debug() {
+            eprintln!("[vk] t={} armed keepalive callback", debug_clock_ms());
+        }
+    }
+}
+
+/// User data of a warm-down frame callback ([`EngineState::keepalive_commit`]),
+/// which clears `keepalive_pending` rather than `frame_callback_pending`.
+pub struct KeepAlive;
+
+impl<A: Application> wayland_client::Dispatch<wl_callback::WlCallback, KeepAlive> for EngineState<A> {
+    fn event(
+        state: &mut Self,
+        _proxy: &wl_callback::WlCallback,
+        event: wl_callback::Event,
+        _data: &KeepAlive,
+        _conn: &Connection,
+        _qh: &QueueHandle<Self>,
+    ) {
+        if let wl_callback::Event::Done { .. } = event {
+            state.keepalive_pending = false;
+            if crate::vk::present_debug() {
+                let waited = state.keepalive_armed_at.map(|a| a.elapsed().as_millis()).unwrap_or(0);
+                eprintln!("[vk] t={} keepalive-done (waited {}ms)", debug_clock_ms(), waited);
+            }
+        }
+    }
+}
+
 impl<A: Application> Drop for EngineState<A> {
     fn drop(&mut self) {
         // The popup's renderer lets go of its surface before the popup (and
@@ -1895,6 +1949,8 @@ fn run_session<'l, A: Application>(
         damage_owed: true,
         frame_callback_pending: false,
         frame_callback_armed_at: None,
+        keepalive_pending: false,
+        keepalive_armed_at: None,
         extent_gate_skips: 0,
         first_configure_received: false,
         driver: Driver::new(),