GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
perf(runner): an isolated update goes to sleep without a warm-down
Every genuine redraw armed the 200 ms WARM_DOWN, ~12 more turns at frame
rate with a bufferless keepalive commit each. That is for interaction and
for streams — commits close together, where a late frame callback would
delay the next one — but an app's own isolated update has no next frame
to delay. The status bar's stats module redraws once a second and so woke
~13 times a second for it.
The warm-down is now armed only when the redraw is part of a sequence:
an input event arrived within SEQUENCE_GAP (500 ms), or the previous
genuine redraw did. Driver stamps `last_input` in every input entry
point (pointer, scroll, touch, pinch, keyboard focus, keys, IME). A
terminal printing or a sync narrating progress keeps its warm-down; a
once-a-second stat or a once-a-minute clock does not.
The stats module alone in a scale-2 shadow, 10 s, three runs each: main
thread 99-164 -> 23-26 wakes, CPU 10-30 -> 10-20 ms. Pacer tests: an
isolated update goes straight to the idle sleep; an input-led redraw
and back-to-back redraws still warm down.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
src/backend/driver.rs | 21 ++++++++++++++++++++
src/backend/shell.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 73 insertions(+), 3 deletions(-)
diff --git a/src/backend/driver.rs b/src/backend/driver.rs
index 982376f..c54da97 100644
--- a/src/backend/driver.rs
+++ b/src/backend/driver.rs
@@ -150,6 +150,10 @@ impl<A: Application> Turn<'_, A> {
/// The input state a session carries, and the routing over it.
pub struct Driver {
pub mods: Modifiers,
+ /// When the last input event of any kind arrived — what makes a redraw
+ /// interactive, which is the redraw the runner's warm-down is for
+ /// (`shell::Pacer`).
+ pub last_input: Option<std::time::Instant>,
pub pressed_key: Option<PressedKey>,
/// The pointer's last position, window-logical (popup events translated).
pub cursor_pos: (f32, f32),
@@ -215,6 +219,7 @@ impl Driver {
Self {
mods: Modifiers::default(),
pressed_key: None,
+ last_input: None,
cursor_pos: (0.0, 0.0),
buttons_down: 0,
last_pinch_scale: 1.0,
@@ -282,12 +287,18 @@ impl Driver {
/// hover state is stale from enter to first move, and a press in that
/// window can misroute (e.g. a divider press falling through to the
/// movable-root plate window drag).
+ fn note_input(&mut self) {
+ self.last_input = Some(std::time::Instant::now());
+ }
+
pub fn pointer_enter<A: Application>(&mut self, t: Turn<'_, A>, pos: LogicalPosition) {
+ self.note_input();
self.pointer_motion(t, pos);
}
/// The pointer moved to `pos`.
pub fn pointer_motion<A: Application>(&mut self, t: Turn<'_, A>, pos: LogicalPosition) {
+ self.note_input();
let mut rebuild = false;
t.app.handle_pointer_move(pos, &mut rebuild);
if rebuild {
@@ -302,6 +313,7 @@ impl Driver {
/// first; then clear hover with an off-screen move — safe now that no
/// drag is held.
pub fn pointer_leave<A: Application>(&mut self, mut t: Turn<'_, A>) {
+ self.note_input();
if self.buttons_down != 0 {
let (px, py) = self.cursor_pos;
for btn in [MouseButton::Left, MouseButton::Right, MouseButton::Middle] {
@@ -336,6 +348,7 @@ impl Driver {
pos: LogicalPosition,
site: PressSite,
) -> Press {
+ self.note_input();
self.buttons_down |= button_bit(btn);
let (lx, ly) = (pos.x, pos.y);
@@ -389,6 +402,7 @@ impl Driver {
/// A button came up at `pos`.
pub fn pointer_release<A: Application>(&mut self, mut t: Turn<'_, A>, btn: MouseButton, pos: LogicalPosition) {
+ self.note_input();
self.buttons_down &= !button_bit(btn);
let mut rebuild = false;
let msg = t.app.handle_mouse_input(btn, ElementState::Released, pos, &mut rebuild);
@@ -398,6 +412,7 @@ impl Driver {
/// One coalesced frame of scrolling at `pos`. Publishes the frame's
/// phase (`scroll_motion::set_scroll_phase`) before the app sees it.
pub fn scroll<A: Application>(&mut self, t: Turn<'_, A>, frame: ScrollFrame, pos: LogicalPosition) {
+ self.note_input();
let ScrollFrame { h, v, discrete_h, discrete_v, source, stop } = frame;
// Per-app scroll factors from input.kdl (`<app>`/`cce-ui` domain
// `input { }` blocks); the compositor's global device scaling has
@@ -438,6 +453,7 @@ impl Driver {
actions: Vec<super::touch::TouchAction>,
scroll_at: Option<(f32, f32)>,
) {
+ self.note_input();
use super::touch::TouchAction;
for action in actions {
let mut rebuild = false;
@@ -514,6 +530,7 @@ impl Driver {
/// The pinch's cumulative `scale` moved, with the pointer where it last was.
pub fn pinch_update<A: Application>(&mut self, t: Turn<'_, A>, scale: f32) {
+ self.note_input();
let factor = scale / self.last_pinch_scale;
self.last_pinch_scale = scale;
@@ -564,6 +581,7 @@ impl Driver {
/// The window gained (`true`) or lost keyboard focus. Losing it drops a
/// held key and the held modifiers, whose releases go elsewhere.
pub fn keyboard_focus<A: Application>(&mut self, t: Turn<'_, A>, focused: bool) {
+ self.note_input();
if !focused {
self.pressed_key = None;
self.mods.ctrl = false;
@@ -587,6 +605,7 @@ impl Driver {
text: Option<String>,
state: ElementState,
) {
+ self.note_input();
let event = KeyEvent {
state,
logical_key,
@@ -648,6 +667,7 @@ impl Driver {
/// commits on its own keys), never repeated, and past the chords: a
/// commit of "z" is a "z", not half of an undo.
pub fn commit_text<A: Application>(&mut self, t: Turn<'_, A>, text: String) {
+ self.note_input();
if text.is_empty() {
return;
}
@@ -678,6 +698,7 @@ impl Driver {
/// commit, or the commit follows). The editing widget shows it from the
/// next frame.
pub fn preedit<A: Application>(&mut self, t: Turn<'_, A>, preedit: Option<crate::ime::Preedit>) {
+ self.note_input();
crate::ime::set_preedit(preedit);
*t.redraw = true;
}
diff --git a/src/backend/shell.rs b/src/backend/shell.rs
index c2b7153..79672fc 100644
--- a/src/backend/shell.rs
+++ b/src/backend/shell.rs
@@ -38,6 +38,18 @@ pub const IDLE_DISPATCH: Duration = Duration::from_millis(1000);
/// compositor.
pub const WARM_DOWN: Duration = Duration::from_millis(200);
+/// A redraw this soon after the previous one, or after input, belongs to a
+/// sequence and gets the [`WARM_DOWN`]; one further from both is an
+/// isolated update and does not.
+///
+/// The warm-down is for interaction and for streams (a terminal printing, a
+/// sync narrating progress): a run of commits close together, where a late
+/// frame callback would delay the next one. An app's own isolated update —
+/// the status bar's stats once a second, a clock once a minute — has no next
+/// frame to delay, and each one used to buy twelve more wakes at frame rate:
+/// the stats module woke ~13 times a second for one redraw (2026-10-06).
+pub const SEQUENCE_GAP: Duration = Duration::from_millis(500);
+
/// Upper bound on an idle sleep. The loop is woken early by any event the
/// shell's window system delivers and by messages on the app's sender, so
/// this only caps how long an app-side poll that bypasses both (see
@@ -117,13 +129,15 @@ pub struct Pacer {
slept_idle: bool,
/// Turns stay at frame rate until this instant (see [`WARM_DOWN`]).
warm_until: Option<Instant>,
+ /// When the last genuine redraw was presented (see [`SEQUENCE_GAP`]).
+ last_redraw: Option<Instant>,
last_title: String,
}
impl Pacer {
/// Before the first turn the loop runs at [`ACTIVE_DISPATCH`].
pub fn new(title: String) -> Self {
- Self { last_tick: Instant::now(), slept_idle: false, warm_until: None, last_title: title }
+ Self { last_tick: Instant::now(), slept_idle: false, warm_until: None, last_redraw: None, last_title: title }
}
/// One turn of the loop. The cadence is ACTIVE while anything is in
@@ -177,9 +191,17 @@ impl Pacer {
let pending = shell.frame_pending();
if *shell.redraw() {
- // Genuine dirt (input, app state, animation) extends the warm window;
+ // Genuine dirt that is part of a sequence — input just arrived, or
+ // the last redraw was moments ago — extends the warm window;
// warm-down renders below do NOT, so idle decays in one window.
- self.warm_until = Some(Instant::now() + WARM_DOWN);
+ // An isolated update gets none (see `SEQUENCE_GAP`).
+ let now = Instant::now();
+ let recent = |t: Option<Instant>| t.is_some_and(|t| now.duration_since(t) < SEQUENCE_GAP);
+ let input = shell.turn().0.last_input;
+ if recent(input) || recent(self.last_redraw) {
+ self.warm_until = Some(now + WARM_DOWN);
+ }
+ self.last_redraw = Some(now);
}
let mut rendered = false;
if *shell.redraw() && !pending {
@@ -346,6 +368,8 @@ mod tests {
#[test]
fn a_redraw_presents_then_warms_down_at_frame_rate() {
let (mut p, mut s) = (Pacer::new(String::new()), Mock::new());
+ // Interactive: an input event just arrived.
+ s.driver.last_input = Some(Instant::now());
s.redraw = true;
assert_eq!(p.turn(&mut s), Step::Sleep(ACTIVE_DISPATCH));
assert_eq!(s.did, vec![Did::Present(true)]);
@@ -356,6 +380,31 @@ mod tests {
assert_eq!(s.did.last(), Some(&Did::Present(false)));
}
+ #[test]
+ fn an_isolated_update_presents_and_goes_idle_without_a_warm_down() {
+ let (mut p, mut s) = (Pacer::new(String::new()), Mock::new());
+ // No input, no recent redraw: the app's own once-a-second update.
+ s.redraw = true;
+ assert_eq!(p.turn(&mut s), Step::Sleep(ACTIVE_DISPATCH));
+ assert_eq!(s.did, vec![Did::Present(true)]);
+ // No warm-down re-render: straight to the idle sleep.
+ assert_eq!(p.turn(&mut s), idle());
+ assert_eq!(s.did, vec![Did::Present(true)], "an isolated update warmed down");
+ }
+
+ #[test]
+ fn a_redraw_soon_after_another_warms_down_without_input() {
+ let (mut p, mut s) = (Pacer::new(String::new()), Mock::new());
+ s.redraw = true;
+ p.turn(&mut s);
+ p.turn(&mut s);
+ // A stream: the next update lands well inside SEQUENCE_GAP.
+ s.redraw = true;
+ assert_eq!(p.turn(&mut s), Step::Sleep(ACTIVE_DISPATCH));
+ assert_eq!(p.turn(&mut s), Step::Sleep(ACTIVE_DISPATCH));
+ assert_eq!(s.did.last(), Some(&Did::Present(false)), "the stream lost its warm-down");
+ }
+
#[test]
fn a_pending_frame_withholds_the_present_and_keeps_the_redraw() {
let (mut p, mut s) = (Pacer::new(String::new()), Mock::new());