fix(presenter): F11 to fullscreen no longer quits the Windows session #443

Merged
enricobuehler merged 2 commits from worktree-win-f11-swapchain into main 2026-08-29 14:38:55 +00:00
3 changed files with 60 additions and 14 deletions
+23 -1
View File
@@ -888,7 +888,29 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
}
}
WindowEvent::PixelSizeChanged(..) | WindowEvent::Resized(..) => {
presenter.recreate_swapchain(&window)?;
// A driver that refuses the new size used to end the SESSION: this
// `?` walked out of `run_session`, and the shell reported a live
// stream as "couldn't connect". Field-reported on Windows 11 as
// F11 → `vkCreateSwapchainKHR: VK_ERROR_UNKNOWN`, with the reverse
// toggle fine. A refused fullscreen swapchain costs the
// fullscreen, not the stream: fall back to the geometry that was
// already working and let the size event that follows rebuild
// against it. A windowed failure still propagates — there is no
// smaller state left to fall back to.
if let Err(e) = presenter.recreate_swapchain(&window) {
if !fullscreen {
return Err(e);
}
tracing::warn!(
error = format!("{e:#}"),
"swapchain recreate failed — leaving fullscreen"
);
fullscreen = false;
if let Err(e) = window.set_fullscreen(false) {
tracing::warn!(error = %e, "failed to leave fullscreen");
}
continue;
}
presenter.present(&window, FrameInput::Redraw, overlay_frame.as_ref())?;
// Match-window (D2): (re)stamp the debounce — the request fires
// once ~400 ms pass with no further size events, never per
+5 -1
View File
@@ -10,7 +10,11 @@
//!
//! Lifecycle: `vkWaitForPresentKHR` requires the swapchain to stay alive for the call's
//! duration, so [`PresentTimer::drain`] must run before any `vkDestroySwapchainKHR`
//! (recreate and teardown both do). Waits carry a 250 ms cap: presentation ids complete
//! (recreate and teardown both do) — AND before a `vkCreateSwapchainKHR` that names the
//! live swapchain as `oldSwapchain`, which externally-synchronises it and lets the driver
//! retire it under a parked waiter. Stating only the destroy half is how the drain came to
//! sit after the create, which is a Windows `VK_ERROR_UNKNOWN` on an F11 mode change.
//! Waits carry a 250 ms cap: presentation ids complete
//! in submission order (a MAILBOX-replaced image's id completes with the present that
//! replaced it), so a wait only outlives that cap when the pipeline is already wedged —
//! the timeout keeps the drain bounded rather than wedging a resize with it.
+32 -12
View File
@@ -79,6 +79,21 @@ impl Presenter {
min_images = min_images.min(caps.max_image_count);
}
// The present-wait waiter is the last thing still holding the live swapchain, and
// `vkCreateSwapchainKHR` externally-synchronises `oldSwapchain` — so the drain
// belongs BEFORE the create, not merely before the destroy. It used to sit after
// the create, which left a waiter parked inside `vkWaitForPresentKHR(old)` while
// the driver retired that same swapchain underneath it. The window that opens is
// widest exactly where the field reports land: a mode change orphans its last
// present, so that wait runs the full 250 ms cap instead of completing at the
// next vblank. Bounded by that same cap, and normally instant.
if let Some(t) = &self.present_timer {
t.drain();
}
// An unclaimed last present belonged to the dying swapchain — drop the claim.
// (`note_presented` is the only producer and shares this thread, so nothing can
// hand the waiter a new job between the drain above and the destroy below.)
self.last_presented = None;
let old = self.swapchain;
let info = vk::SwapchainCreateInfoKHR::default()
.surface(self.surface)
@@ -99,19 +114,24 @@ impl Presenter {
// SAFETY: per the Vulkan contract above - a create/allocate call on the live device, over
// builder structs that are locals outliving the call; the handle it returns is owned by
// the value being built here.
let swapchain = unsafe { self.swap_d.create_swapchain(&info, None) }
.map_err(|e| anyhow!("vkCreateSwapchainKHR: {e}{}", kmsdrm_swapchain_hint()))?;
// The parameters ride the failure text: a swapchain refusal is otherwise a bare
// driver code, and the first question every field report raises — which size,
// which format, which present mode — cost nothing to answer here.
let swapchain = unsafe { self.swap_d.create_swapchain(&info, None) }.map_err(|e| {
anyhow!(
"vkCreateSwapchainKHR: {e} ({}x{}, {:?} / {:?}, {:?}, {min_images} images){}",
extent.width,
extent.height,
self.format.format,
self.format.color_space,
self.present_mode,
kmsdrm_swapchain_hint()
)
})?;
// The old swapchain and everything tied to its images dies NOW: the fence
// quiesce covered our own command buffers, the queue drain above covered the
// presentation engine's semaphore waits — nothing can still reference them.
// The present-wait waiter is the one remaining referent: `vkWaitForPresentKHR`
// requires the swapchain alive for the call, so drain it first (bounded by the
// waiter's 250 ms cap; ids complete in order so this is normally instant).
if let Some(t) = &self.present_timer {
t.drain();
}
// An unclaimed last present belonged to the dying swapchain — drop the claim.
self.last_presented = None;
// quiesce covered our own command buffers, the queue drain covered the
// presentation engine's semaphore waits, and the present-wait drain above
// released the last referent — nothing can still reference them.
let (overlay_views, overlay_framebuffers) = self.overlay_pipe.take_targets();
// SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by this
// type and live for the call, and every builder struct is a local that outlives it.