cloud storage client
git clone https://git.lucas.co/cce-cloud.git
Draw no popup frame before the surface's first configure
With the kept renderer (7014b9f) a popup is ready in microseconds, and
the Super-Tab switcher's window list, streamed in right after the
request, asked for a frame before the layer surface's first configure
arrived. Mesa attached a buffer to the unconfigured surface, the
compositor disconnected the client for it, and the daemon exited -- with
status 0, which systemd does not restart. A newly built renderer used to
take long enough that the configure always won. State::render now draws
nothing until a configure handler has run; both already ask for a redraw.
Also logs "open -> first frame with rows" for list popups, since a
streamed list's first frame can be empty.
In a shadow: 20 switcher opens served without a disconnect (both builds
since 7014b9f lost the daemon on the first or second), on screen with
rows at a 31 ms median against 317 ms for the build before 7014b9f;
96/96 menu and launcher popups drawn; switcher, menu and launcher
screenshots pixel-identical to that build's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 13 ++++++++++++-
src/main.rs | 38 +++++++++++++++++++++++++++++++++++---
2 files changed, 47 insertions(+), 4 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 178be60..1d1cdba 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -232,7 +232,18 @@ startup on a scratch surface that is never mapped, so the first popup attaches
too. Until 2026-10-05 every popup built a new renderer, and its pipelines were
70-100 ms of a ~100 ms popup on an idle machine and several hundred under
load; a reopen is now one swapchain, and a menu popup is ready in ~2 ms and on
-screen in 15-40 ms, the launcher in 30-60. An image uploaded to it lives as
+screen in 15-40 ms, the launcher in 30-60. **A popup draws nothing before its
+surface's first configure** (`State::configured`, checked in `State::render`):
+a buffer attached ahead of a layer surface's configure is a protocol error,
+and the compositor disconnects the daemon, and with it every popup after.
+The renderer used to take long enough to build that the configure always
+won; once it was kept, the Super-Tab switcher's streamed rows asked for a
+frame first and killed the daemon on its first open (fixed the same day,
+before it reached a release that stayed installed). Note a shadow's idle
+timeout turns its output off after 10 minutes, after which xdg toplevels
+(the launcher) are never configured at all; `ccectl idle timeouts 0 0`
+first. `open -> first frame with rows` is logged for list popups: a
+streamed list's first frame can be empty. An image uploaded to it lives as
long as it does: upload through a cache like `icon_image`, never per popup,
or free it at close. Standalone mode still builds its own.
diff --git a/src/main.rs b/src/main.rs
index b07489b..019acd9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1719,6 +1719,20 @@ struct State {
/// app_id → icon name for the switcher's rows ([`desktop_icon_index`]),
/// built on the first row that needs it and kept for the popup's life.
switcher_icon_index: Option<std::collections::HashMap<String, String>>,
+ /// When `State::new` began, and whether the first frame that shows any
+ /// rows has been logged since. A streamed list (Dmenu, the switcher)
+ /// arrives after the popup opens, so the first frame alone can be an
+ /// empty list; this is the one that shows the user something to pick.
+ opened_at: std::time::Instant,
+ rows_frame_logged: bool,
+ /// Whether the compositor has configured the surface yet. Nothing may be
+ /// drawn before: a buffer attached ahead of a layer surface's first
+ /// configure is a protocol error, and the compositor disconnects the
+ /// whole client, which for the daemon is every popup after it. Until
+ /// 2026-10-05 the renderer took long enough to build that the configure
+ /// always won; with the daemon's kept renderer a popup is ready in
+ /// microseconds, and the switcher's streamed rows asked for a frame first.
+ configured: bool,
last_tick: std::time::Instant,
ui_context: cce_ui::context::UiContext,
/// Dissolved root plate container (Phase 6as): the plate was a pure value-holder for the
@@ -2102,6 +2116,9 @@ impl State {
select_item,
switcher_mode,
switcher_icon_index: None,
+ opened_at: t_start,
+ rows_frame_logged: false,
+ configured: false,
last_tick: std::time::Instant::now(),
ui_context: cce_ui::context::UiContext::new(),
window_rect,
@@ -2195,7 +2212,9 @@ impl State {
if new.is_empty() {
return;
}
+ let t = std::time::Instant::now();
let index = index.get_or_insert_with(|| desktop_icon_index(&application_dirs()));
+ log::debug!("[timing] switcher icon index: {:?}", t.elapsed());
let icons: Vec<(String, (u32, u32, u32))> = new
.into_iter()
.filter_map(|item| {
@@ -2465,7 +2484,13 @@ impl State {
}
}
+ /// Draw a frame, or nothing before the surface's first configure (see
+ /// `State::configured`): true if a frame was drawn. The configure
+ /// handlers ask for a redraw, so a skipped frame is drawn right after it.
fn render(&mut self) -> bool {
+ if !self.configured {
+ return false;
+ }
let now = std::time::Instant::now();
self.last_tick = now;
@@ -2481,7 +2506,11 @@ impl State {
// Always a full frame: the popup is small and repaints whole.
damage: None,
});
- false
+ if !self.rows_frame_logged && !self.fuzzel.filtered_items.is_empty() {
+ self.rows_frame_logged = true;
+ log::info!("[timing] open -> first frame with rows: {:?}", self.opened_at.elapsed());
+ }
+ true
}
}
@@ -3158,6 +3187,7 @@ impl LayerShellHandler for AppState {
let pw = (width as f64 * state.scale) as u32;
let ph = (height as f64 * state.scale) as u32;
state.resize(pw, ph);
+ state.configured = true;
}
self.redraw = true;
}
@@ -3172,6 +3202,9 @@ impl WindowHandler for AppState {
configure: WindowConfigure,
_serial: u32,
) {
+ if let Some(state) = &mut self.state {
+ state.configured = true;
+ }
let (w, h) = configure.new_size;
if let (Some(w), Some(h)) = (w, h) {
let width = w.get();
@@ -4375,8 +4408,7 @@ fn run_daemon(socket_path: &str) {
if app.redraw {
app.redraw = false;
if let Some(st) = &mut app.state {
- let _ = st.render();
- if !first_frame_logged {
+ if st.render() && !first_frame_logged {
first_frame_logged = true;
log::info!("[timing] request -> first frame: {:?}", t_request.elapsed());
}