fix(presenter): F11 to fullscreen no longer quits the Windows session #443
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user