GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
fix: present only the exact extent the logical size and scale call for
The resume-bounce guard checked divisibility, but an even-sized
old-scale buffer divides cleanly by the new scale: the mispaired
commit is protocol-legal, the compositor reads it as a self-resize,
and the window comes back from suspend at exactly half size (the
odd-size case at least dies loudly). Root observed in the wild: the
swapchain rebuild honored surface caps over the requested extent and
then overwrote desired_extent with the result, so pending_extent
could never disagree again and the halved size stuck.
- window_runner: one buffer_geometry() formula feeds both resize()
and the render guard; render skips (and re-requests) any pending
extent that isn't exactly logical x scale, before the frame-callback
request. A skipped present now also clears frame_callback_pending —
the out-of-date path could stall the demand-driven loop waiting on
a callback no commit latches.
- vk/renderer: a rebuild that can't honor the requested extent keeps
the request, requeues itself, and draw_frame skips the present
instead of committing a wrong-size buffer.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/backend/window_runner.rs | 90 ++++++++++++++++++++++++++------------------
src/vk/renderer.rs | 30 +++++++++++++--
2 files changed, 79 insertions(+), 41 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 029e766..ff2347a 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -2640,23 +2640,35 @@ impl<A: Application> EngineState<A> {
self.logical_height = height_logical;
}
+ /// Buffer scale and physical extent for a logical size under the current
+ /// scale factor: rounded, then snapped up so the extent divides by the
+ /// buffer scale (a wl_surface requirement). In forced-scale mode the
+ /// surface stays at buffer_scale 1 (the compositor believes scale 1).
+ ///
+ /// This is the single source of the buffer-size formula: `resize` sizes
+ /// the swapchain with it and `render` refuses to present any extent that
+ /// disagrees with it — a mispaired buffer/scale commit is how the resume
+ /// output bounce halved even-sized windows (buffer at the old scale's
+ /// size, new scale latched; the compositor reads it as a self-resize).
+ fn buffer_geometry(scale_factor: f64, w: f32, h: f32) -> (i32, u32, u32) {
+ let s = if crate::scale::forced_scale().is_some() {
+ 1
+ } else {
+ (scale_factor.round() as i32).max(1)
+ };
+ let su = s as u32;
+ let pw = ((w as f64 * scale_factor).round() as u32).max(1).div_ceil(su) * su;
+ let ph = ((h as f64 * scale_factor).round() as u32).max(1).div_ceil(su) * su;
+ (s, pw, ph)
+ }
+
pub fn resize(&mut self, w: f32, h: f32) {
let (w, h) = self.inner.as_ref().unwrap().adjust_size(w, h);
if w > 0.0 && h > 0.0 {
self.logical_width = w;
self.logical_height = h;
+ let (_, pw, ph) = Self::buffer_geometry(self.scale_factor, w, h);
if let Some(ref mut renderer) = self.renderer {
- // wl_surface requires buffer dimensions divisible by the buffer
- // scale; snap up so a fractional logical size can't queue an
- // illegal swapchain extent. In forced-scale mode the surface
- // stays at buffer_scale 1 (the compositor believes scale 1).
- let s = if crate::scale::forced_scale().is_some() {
- 1
- } else {
- (self.scale_factor.round() as u32).max(1)
- };
- let pw = ((w as f64 * self.scale_factor).round() as u32).max(1).div_ceil(s) * s;
- let ph = ((h as f64 * self.scale_factor).round() as u32).max(1).div_ceil(s) * s;
renderer.resize(pw, ph);
}
let scale = self.scale_factor;
@@ -2920,35 +2932,32 @@ impl<A: Application> EngineState<A> {
// Commit the buffer scale together with a buffer it is legal for: the
// present inside draw_frame_2d is the only commit on this surface, so
// sending the request here orders it right before a matching-size
- // attach+commit. Skipped while the pending extent isn't divisible (a
- // transition frame) — the old committed scale stays legal for it.
+ // attach+commit.
+ //
+ // Present only the EXACT extent the current logical size and scale
+ // call for. Divisibility is not enough: mid scale-transition (the
+ // resume output bounce) the pending extent can belong to the other
+ // scale, and an even-sized old-scale buffer divides cleanly by the
+ // new scale — the commit is protocol-legal, so the compositor reads
+ // it as a self-resize to half/double and reconfigures the window to
+ // match (how the color editor came back from suspend at exactly half
+ // size with the divisibility guard green). Odd sizes at least die
+ // loudly (invalid_size). On mismatch, re-request the right extent
+ // and skip — before the frame-callback request below, so the loop
+ // isn't left waiting on a callback no commit will ever latch.
if let Some(ref surface) = self.surface {
- let s = if crate::scale::forced_scale().is_some() {
- 1
- } else {
- (self.scale_factor.round() as i32).max(1)
- };
+ let (s, epw, eph) =
+ Self::buffer_geometry(self.scale_factor, self.logical_width, self.logical_height);
let e = renderer.pending_extent();
- if s != self.committed_buffer_scale
- && e.width % s as u32 == 0
- && e.height % s as u32 == 0
- {
- surface.set_buffer_scale(s);
- self.committed_buffer_scale = s;
- }
- // Never present a buffer the latching scale can't legally
- // describe: mid scale-transition (resume output bounce) the
- // extent can belong to the other scale, and committing it is a
- // fatal invalid_size for odd sizes and a half/double-size window
- // for even ones. Skip the frame — before the frame-callback
- // request below, so the loop isn't left waiting on a callback no
- // commit will ever latch; the next resize/scale event re-syncs
- // extent and scale and redraws.
- let latching = self.committed_buffer_scale.max(1) as u32;
- if e.width % latching != 0 || e.height % latching != 0 {
+ if e.width != epw || e.height != eph {
+ renderer.resize(epw, eph);
self.redraw = true;
return;
}
+ if s != self.committed_buffer_scale {
+ surface.set_buffer_scale(s);
+ self.committed_buffer_scale = s;
+ }
}
if let Some(ref surface) = self.surface {
@@ -2965,14 +2974,21 @@ impl<A: Application> EngineState<A> {
self.redraw = true;
}
- renderer.draw_frame_2d(Frame2D {
+ if !renderer.draw_frame_2d(Frame2D {
verts: &verts,
batches: &batches,
overlay_verts: &overlay_verts,
images: &image_quads,
plate_features: &plate_features,
clear_color,
- });
+ }) {
+ // No present happened (swapchain out-of-date, or the created
+ // swapchain didn't match the requested extent). The frame
+ // callback requested above will never latch without a commit —
+ // clear it or the demand-driven loop stalls waiting forever.
+ self.frame_callback_pending = false;
+ self.redraw = true;
+ }
}
}
diff --git a/src/vk/renderer.rs b/src/vk/renderer.rs
index 704c18b..db96263 100644
--- a/src/vk/renderer.rs
+++ b/src/vk/renderer.rs
@@ -976,10 +976,26 @@ 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;
+ if extent.width == self.desired_extent.width && extent.height == self.desired_extent.height {
+ // 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;
+ } else {
+ // The surface capabilities overrode the requested size (seen
+ // on suspend/resume, when caps briefly lag the real surface
+ // state). Presenting this swapchain would commit a buffer the
+ // caller never approved — paired with the wrong buffer scale
+ // that reads as a self-resize and half/double-sizes the
+ // window. Keep the request, requeue the rebuild, and let
+ // draw_frame skip the present until caps agree.
+ log::warn!(
+ "swapchain extent {}x{} != requested {}x{}; skipping present until they agree",
+ extent.width, extent.height,
+ self.desired_extent.width, self.desired_extent.height,
+ );
+ self.swapchain_dirty = true;
+ }
let images = self
.swapchain_loader
@@ -1402,6 +1418,12 @@ impl VkRenderer {
if self.swapchain_dirty {
self.swapchain_dirty = false;
self.recreate_swapchain();
+ if self.swapchain_dirty {
+ // The rebuild couldn't honor the requested extent (surface
+ // caps disagree, e.g. mid suspend/resume) — presenting it
+ // would commit a wrong-size buffer. Skip; the caller redraws.
+ return false;
+ }
}
unsafe {