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

commit8d27ae77c96edd2bf93235fe85cae850a0fd9c11
parent74c03b956b
authorLucas Galante <[email protected]>
date2026-07-18 12:58
fix: lay text out against the extent the frame will actually render at

VkRenderer::resize is lazy — it records desired_extent and flags the
swapchain dirty, leaving self.extent at the old size until the next
draw_frame rebuilds. prepare_text ran before that rebuild and passed the
stale extent to glyphon, so every frame following a resize converted pixel
coordinates to NDC against the previous resolution: text rendered
mis-scaled and offset until the sizes reconverged. Quads were unaffected —
they derive NDC from the logical size.

Visible in cce-cloud, which auto-sizes per keystroke: text shrank and
drifted while the list filtered, then snapped back.

prepare_text now uses pending_extent() (desired when a rebuild is queued,
live otherwise), and create_swapchain keeps desired_extent in step with
extent so a rebuild queued for a non-resize reason (suboptimal /
out-of-date acquire) or a caps-clamped extent can't feed it a stale size.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

 src/vk/renderer.rs | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/src/vk/renderer.rs b/src/vk/renderer.rs
index c4f2738..c92b9f3 100644
--- a/src/vk/renderer.rs
+++ b/src/vk/renderer.rs
@@ -828,6 +828,10 @@ impl VkRenderer {
                 self.swapchain_loader.destroy_swapchain(old_swapchain, None);
             }
             self.extent = extent;
+            // Keep the two in step so a rebuild queued for a non-resize reason
+            // (suboptimal/out-of-date) doesn't hand `pending_extent` a stale or
+            // unclamped size.
+            self.desired_extent = extent;
 
             let images = self
                 .swapchain_loader
@@ -995,6 +999,12 @@ impl VkRenderer {
         self.rt.as_ref().is_some_and(|rt| rt.accumulating())
     }
 
+    /// The extent the next `draw_frame` will render at: the pending size when a
+    /// swapchain rebuild is queued, otherwise the live one.
+    fn pending_extent(&self) -> vk::Extent2D {
+        if self.swapchain_dirty { self.desired_extent } else { self.extent }
+    }
+
     /// Request a new physical size (from xdg configure / scale changes). Applied
     /// lazily on the next `draw_frame`.
     pub fn resize(&mut self, width: u32, height: u32) {
@@ -1023,7 +1033,11 @@ impl VkRenderer {
         swash_cache: &mut glyphon::SwashCache,
         spans: &[TextSpan<'_>],
     ) {
-        self.text.prepare(font_system, swash_cache, spans, self.extent);
+        // Against the extent this frame will actually be drawn at: `resize` is
+        // lazy, so with a rebuild pending `self.extent` is still the previous
+        // size and text would land in the wrong NDC (visibly mis-scaled and
+        // offset while a window auto-sizes to its content).
+        self.text.prepare(font_system, swash_cache, spans, self.pending_extent());
     }
 
     /// Render one frame of plain 2D geometry: a single unclipped batch, no