GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
fix: dropped resize request wedged the pending extent, freezing all presents
VkRenderer::resize only recorded the request when it differed from the LIVE
extent. With a rebuild already queued (swapchain_dirty), a request returning
to the live size was silently dropped: desired_extent stayed wedged at the
intermediate size, the render loop's pending_extent gate never matched again,
and every subsequent frame skipped its present — silently, at loop cadence,
with the process otherwise healthy.
Seen in the wild as the status-bar clock frozen after suspend/resume: the
scale bounce (2→1→2 before any draw) resized the clock segment 305→297→305
logical; the return to 305 was the dropped request. The clock rebuilt its
layout every minute for 45 minutes while the compositor never received
another buffer.
resize() now records desired_extent unconditionally, so a queued rebuild is
always retargeted to the latest request. Also add a rate-limited warn when
the extent gate skips ~5s of consecutive renders — this wedge had zero log
footprint, and any future variant should at least say so.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/backend/window_runner.rs | 16 ++++++++++++++++
src/vk/renderer.rs | 9 ++++++++-
2 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 008ac0b..3b3d563 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -2738,6 +2738,10 @@ pub struct EngineState<A: Application> {
/// When the pending frame callback was armed — the starvation fallback's
/// clock (see the render gate in `run`).
pub frame_callback_armed_at: Option<std::time::Instant>,
+ /// Consecutive renders skipped by the extent gate (pending swapchain size
+ /// != the size the current logical size and scale call for). Normally 0 or
+ /// 1; a persistent count means no frame is presenting and deserves a warn.
+ pub extent_gate_skips: u32,
pub first_configure_received: bool,
pub ctrl_pressed: bool,
pub shift_pressed: bool,
@@ -3130,9 +3134,20 @@ impl<A: Application> EngineState<A> {
let e = renderer.pending_extent();
if e.width != epw || e.height != eph {
renderer.resize(epw, eph);
+ self.extent_gate_skips += 1;
+ // ~5s of continuous skipping at the 16ms loop cadence: nothing
+ // is presenting and nothing else will say so — this is the
+ // only witness to a wedged pending extent.
+ if self.extent_gate_skips % 300 == 0 {
+ log::warn!(
+ "[window_runner] extent gate: pending {}x{} != expected {}x{} for {} consecutive renders; no frame is presenting",
+ e.width, e.height, epw, eph, self.extent_gate_skips,
+ );
+ }
self.redraw = true;
return;
}
+ self.extent_gate_skips = 0;
if s != self.committed_buffer_scale {
surface.set_buffer_scale(s);
self.committed_buffer_scale = s;
@@ -4097,6 +4112,7 @@ pub fn run<A: Application>() {
redraw: false,
frame_callback_pending: false,
frame_callback_armed_at: None,
+ extent_gate_skips: 0,
first_configure_received: false,
ctrl_pressed: false,
shift_pressed: false,
diff --git a/src/vk/renderer.rs b/src/vk/renderer.rs
index edcd8a0..8bdfefd 100644
--- a/src/vk/renderer.rs
+++ b/src/vk/renderer.rs
@@ -1363,8 +1363,15 @@ impl VkRenderer {
/// lazily on the next `draw_frame`.
pub fn resize(&mut self, width: u32, height: u32) {
let extent = vk::Extent2D { width: width.max(1), height: height.max(1) };
+ // Record the request unconditionally, not just when it differs from the
+ // live extent: with a rebuild already queued (`swapchain_dirty`), a
+ // request that returns to the live size must overwrite the queued one.
+ // Otherwise `desired_extent` stays wedged at the intermediate size, the
+ // caller's `pending_extent` gate never matches, and no frame presents
+ // again — a resume scale bounce (2→1→2 before any draw) froze the
+ // status-bar clock exactly this way.
+ self.desired_extent = extent;
if extent.width != self.extent.width || extent.height != self.extent.height {
- self.desired_extent = extent;
self.swapchain_dirty = true;
}
}