GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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(),