git.lucas.co / cce-preview
document and image viewer
git clone https://git.lucas.co/cce-preview.git

commit2d0bd32eaa7e6672fabba61202eeec1e781a0495
parent51f0a846c5
authorLucas Galante <[email protected]>
date2026-09-19 00:45
Resident pages belong to a renderer, not to the process

An image id names an entry in one renderer's image table, and a renderer
does not outlive its session: cce-ui's window_runner repairs a lost
Wayland transport by opening a new session around the same Application,
which rebuilds the VkRenderer and with it the image table. A draw for an
id that table does not hold is skipped rather than reported.

PageStore caches exactly those ids, and a resident page is never
re-rendered — so a reconnected viewer came back with its chrome, its
title and its zoom over a blank document, and stayed that way until the
file was reopened. Reproduced in a shadow session with
CCE_UI_FAULT_RECONNECT=8: before the injected drop the page draws, after
it the window is empty white.

renderer_init on any renderer after the first calls store.reset(), which
already does the right thing in both directions: it frees every resident
page, and it bumps the generation so a render still in flight for the
old session is dropped on arrival rather than cached as a page that
would draw nothing. The next display_list finds nothing resident and
queues the visible pages again. Same shadow run with the fix: the page
is back on the frame after the drop.

Not on the first renderer — the pages queued from new() are waiting for
precisely that one. window_runner documents the contract above `run`.

 src/main.rs | 31 +++++++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)

diff --git a/src/main.rs b/src/main.rs
index 39088c2..b5f8345 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -69,6 +69,10 @@ struct PreviewApp {
     drag: Option<(f64, f64)>,
     ctrl: bool,
     shift: bool,
+    /// Whether a renderer has been handed over yet — the first one is the
+    /// process's own, any later one is a replacement after a reconnect.
+    /// See `renderer_init`.
+    seen_renderer: bool,
 }
 
 /// Per-page layout rect in document units.
@@ -305,6 +309,7 @@ impl Application for PreviewApp {
             drag: None,
             ctrl: false,
             shift: false,
+            seen_renderer: false,
         };
         let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
         match args.len() {
@@ -358,6 +363,32 @@ impl Application for PreviewApp {
         }
     }
 
+    /// Throw the resident pages away when the renderer is replaced.
+    ///
+    /// `PageStore` holds **renderer** image ids, and a renderer does not
+    /// outlive its session: `cce-ui`'s `window_runner` repairs a lost Wayland
+    /// transport by opening a new session around the same `Application`, which
+    /// rebuilds the renderer and with it the image table. The cached ids then
+    /// name images that no longer exist, and a draw for an unknown id is
+    /// skipped rather than reported — so a reconnected viewer came back with
+    /// its chrome and a blank document, and stayed that way, because a
+    /// resident page is never re-rendered.
+    ///
+    /// `reset` is exactly the right hammer: it frees every page (a free for an
+    /// id the new renderer never had is a no-op) and bumps the generation, so
+    /// a render still in flight for the old session is dropped on arrival
+    /// instead of landing as a page nobody asked for. The next `display_list`
+    /// finds nothing resident and queues the visible pages again.
+    ///
+    /// Not on the first renderer: the pages queued from `new()` are waiting
+    /// for precisely that one.
+    fn renderer_init(&mut self, _renderer: &mut cce_ui::vk::VkRenderer) {
+        if std::mem::replace(&mut self.seen_renderer, true) {
+            log::info!("[preview] renderer replaced; re-rendering the resident pages");
+            self.store.reset();
+        }
+    }
+
     fn handle_resize(&mut self, width: f32, height: f32, scale: f64) {
         self.win = (width, height);
         self.scale = scale;