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
Owner

Field report, 0.33.0, Win 11 client and Win 11 host. Streaming windowed is
fine. F11 to fullscreen quits the session with:

presenter: vkCreateSwapchainKHR: An unknown error has occurred, due to an
implementation or application bug

Fullscreen to windowed works. The shell renders it as "Couldn't connect",
which is wrong — the stream was live. That string is just how a non-zero
session exit is displayed.

Placing the failure

The reported text pins the call site without needing a log.
clients/session/src/main.rs formats a run_session Err as
presenter: {e:#}. A startup failure would read
presenter: vulkan presenter: vkCreateSwapchainKHR: …, because
Presenter::new carries .context("vulkan presenter"). One prefix only, so
this is the mid-session recreate_swapchain — the resize handler that
F11 drives, not swapchain creation at connect.

Cause

vk/reconfig.rs::recreate_swapchain drained the present-wait waiter after
calling vkCreateSwapchainKHR(oldSwapchain = old). The pf-present-wait
thread could therefore be parked inside vkWaitForPresentKHR(old) while the
driver retired that same swapchain.

vkCreateSwapchainKHR externally-synchronises oldSwapchain, so no other
thread may touch it for the duration of the call. The overlap is a spec
violation, and VK_ERROR_UNKNOWN is a normal Windows driver response to one.

present_timing.rs's own module doc stated only the destroy half of the
rule — "drain must run before any vkDestroySwapchainKHR". That is how the
drain came to sit on the wrong side of the create. The doc now states both
halves, so the next reader cannot re-break it the same way.

Why this fits the reported asymmetry. A mode change orphans its last
present, so that wait runs its full 250 ms cap instead of completing at
the next vblank. The overlap window is widest going into fullscreen, and
narrow coming back out. That is a fit, not a proof: no GPU-less test can
cover it, and neither maintainer box is the reporter's hardware.

Second hypothesis, if it survives this. Hybrid graphics. pick_device
prefers the discrete GPU, and a fullscreen swapchain on an adapter that does
not drive the display is a known Windows refusal. PUNKTFUNK_VK_DEVICE=<n>
moves it — note the index is raw vkEnumeratePhysicalDevices order, not
--probe-decode's discrete-first display order. The richer failure text
below is what makes that call from a field log instead of a guess.

Changes

  1. reconfig.rs — the drain, and the unclaimed-present claim drop, move
    above the create. note_presented is the only producer and shares the
    render thread, so nothing can hand the waiter a new job in between.
  2. run.rs — a refused swapchain costs the fullscreen, not the
    session
    . The resize arm's bare ? walked all the way out of
    run_session. It now warns, drops back to windowed, and rebuilds against
    the geometry that was already working. A windowed failure still
    propagates, since there is no smaller state to fall back to. This is right
    regardless of which hypothesis above is correct: a transient driver
    refusal during a mode transition should never end a stream.
  3. reconfig.rs — the vkCreateSwapchainKHR failure text now carries
    extent, format, colour space, present mode and image count. Those are the
    first questions any swapchain report raises, and they cost nothing here.

Deliberately skipped

A PUNKTFUNK_PRESENT_WAIT=0 kill switch, matching the PUNKTFUNK_HDR10=0
explicit-off grammar already in that file, to A/B the waiter race in the
field without a rebuild. Add it if the fix does not hold; it costs a line in
scripts/ci/docs-undocumented-env-baseline.txt or a docs entry.

Levers that already exist on 0.33.0, for anyone reproducing this:
PUNKTFUNK_PRESENT_MODE=fifo (the default ladder picks MAILBOX first for
vsync-on without the VRR opt-in) and PUNKTFUNK_HDR10=0.

Verification

Check Result
cargo fmt --all -- --check clean, workspace-wide
Linux cargo clippy --all-targets -p pf-presenter -- -D warnings green
Windows cargo clippy --all-targets -p pf-presenter -- -D warnings green, cold 3m01s on the CI runner
cargo test -p pf-presenter 54 passed

Both clippy runs were checked for non-vacuity (Checking pf-presenter present
in each log), because cargo check -p pf-presenter on macOS compiles
nothing
: every module in its lib.rs is #[cfg(any(target_os = "linux", windows))], so it "passes" in 0.3 s and reports 0 tests where the real count
is 54. scripts/xcheck.sh does not cover this crate either, so the Windows
run was done on the runner rather than inferred.

Neither behavioural change has an automated check. Both need a GPU and a
window. The real verification is the reporter's box, and this PR should be
treated as unconfirmed on glass until that happens.

User-facing fact changed? n/a — no install step, knob, port, limit or
documented behaviour changes. The only new user-visible string is the extra
detail appended to an existing error message.

Field report, 0.33.0, Win 11 client and Win 11 host. Streaming windowed is fine. **F11 to fullscreen quits the session** with: ``` presenter: vkCreateSwapchainKHR: An unknown error has occurred, due to an implementation or application bug ``` Fullscreen to windowed works. The shell renders it as **"Couldn't connect"**, which is wrong — the stream was live. That string is just how a non-zero session exit is displayed. ## Placing the failure The reported text pins the call site without needing a log. `clients/session/src/main.rs` formats a `run_session` `Err` as `presenter: {e:#}`. A **startup** failure would read `presenter: vulkan presenter: vkCreateSwapchainKHR: …`, because `Presenter::new` carries `.context("vulkan presenter")`. One prefix only, so this is the **mid-session `recreate_swapchain`** — the resize handler that F11 drives, not swapchain creation at connect. ## Cause `vk/reconfig.rs::recreate_swapchain` drained the present-wait waiter **after** calling `vkCreateSwapchainKHR(oldSwapchain = old)`. The `pf-present-wait` thread could therefore be parked inside `vkWaitForPresentKHR(old)` while the driver retired that same swapchain. `vkCreateSwapchainKHR` externally-synchronises `oldSwapchain`, so no other thread may touch it for the duration of the call. The overlap is a spec violation, and `VK_ERROR_UNKNOWN` is a normal Windows driver response to one. `present_timing.rs`'s own module doc stated only the **destroy** half of the rule — "`drain` must run before any `vkDestroySwapchainKHR`". That is how the drain came to sit on the wrong side of the create. The doc now states both halves, so the next reader cannot re-break it the same way. **Why this fits the reported asymmetry.** A mode change orphans its last present, so that wait runs its full **250 ms cap** instead of completing at the next vblank. The overlap window is widest going *into* fullscreen, and narrow coming back out. That is a fit, not a proof: no GPU-less test can cover it, and neither maintainer box is the reporter's hardware. **Second hypothesis, if it survives this.** Hybrid graphics. `pick_device` prefers the discrete GPU, and a fullscreen swapchain on an adapter that does not drive the display is a known Windows refusal. `PUNKTFUNK_VK_DEVICE=<n>` moves it — note the index is raw `vkEnumeratePhysicalDevices` order, not `--probe-decode`'s discrete-first display order. The richer failure text below is what makes that call from a field log instead of a guess. ## Changes 1. **`reconfig.rs`** — the drain, and the unclaimed-present claim drop, move above the create. `note_presented` is the only producer and shares the render thread, so nothing can hand the waiter a new job in between. 2. **`run.rs`** — a refused swapchain costs the **fullscreen, not the session**. The resize arm's bare `?` walked all the way out of `run_session`. It now warns, drops back to windowed, and rebuilds against the geometry that was already working. A windowed failure still propagates, since there is no smaller state to fall back to. This is right regardless of which hypothesis above is correct: a transient driver refusal during a mode transition should never end a stream. 3. **`reconfig.rs`** — the `vkCreateSwapchainKHR` failure text now carries extent, format, colour space, present mode and image count. Those are the first questions any swapchain report raises, and they cost nothing here. ## Deliberately skipped A `PUNKTFUNK_PRESENT_WAIT=0` kill switch, matching the `PUNKTFUNK_HDR10=0` explicit-off grammar already in that file, to A/B the waiter race in the field without a rebuild. Add it if the fix does not hold; it costs a line in `scripts/ci/docs-undocumented-env-baseline.txt` or a docs entry. Levers that already exist on 0.33.0, for anyone reproducing this: `PUNKTFUNK_PRESENT_MODE=fifo` (the default ladder picks **MAILBOX** first for vsync-on without the VRR opt-in) and `PUNKTFUNK_HDR10=0`. ## Verification | Check | Result | | --- | --- | | `cargo fmt --all -- --check` | clean, workspace-wide | | Linux `cargo clippy --all-targets -p pf-presenter -- -D warnings` | green | | **Windows** `cargo clippy --all-targets -p pf-presenter -- -D warnings` | green, cold 3m01s on the CI runner | | `cargo test -p pf-presenter` | 54 passed | Both clippy runs were checked for non-vacuity (`Checking pf-presenter` present in each log), because **`cargo check -p pf-presenter` on macOS compiles nothing**: every module in its `lib.rs` is `#[cfg(any(target_os = "linux", windows))]`, so it "passes" in 0.3 s and reports 0 tests where the real count is 54. `scripts/xcheck.sh` does not cover this crate either, so the Windows run was done on the runner rather than inferred. **Neither behavioural change has an automated check.** Both need a GPU and a window. The real verification is the reporter's box, and this PR should be treated as unconfirmed on glass until that happens. **User-facing fact changed?** n/a — no install step, knob, port, limit or documented behaviour changes. The only new user-visible string is the extra detail appended to an existing error message.
enricobuehler added 2 commits 2026-08-29 14:29:10 +00:00
Toggling fullscreen mid-stream on Windows 11 quit the session with
`vkCreateSwapchainKHR: VK_ERROR_UNKNOWN`.

`recreate_swapchain` drained the present-wait waiter AFTER calling
`vkCreateSwapchainKHR(oldSwapchain = old)`, so the pf-present-wait
thread could sit inside `vkWaitForPresentKHR(old)` while the driver
retired that same swapchain. `vkCreateSwapchainKHR` externally-
synchronises `oldSwapchain`, so the overlap is a spec violation.

The drain (and the unclaimed-present drop) now run before the create.
`present_timing.rs` stated only the destroy half of the rule, which is
how the drain came to sit on the wrong side; it now states both.

The failure text also carries extent, format, colour space, present
mode and image count -- the first questions a field report raises.
fix(presenter): a refused swapchain costs fullscreen, not the session
ci / bun-nix (pull_request) Successful in 34s
ci / web (pull_request) Successful in 52s
ci / docs-drift (pull_request) Successful in 40s
ci / docs-site (pull_request) Successful in 1m14s
ci / rust-arm64 (pull_request) Successful in 1m33s
android / android (pull_request) Successful in 7m20s
ci / rust (pull_request) Successful in 10m3s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 10m56s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 4m19s
09662c0466
A `recreate_swapchain` failure in the resize handler propagated out of
`run_session`, ending the stream; the shell then reported a live
session as "Couldn't connect".

A driver that refuses the fullscreen-sized swapchain now costs the
fullscreen only: warn, drop back to windowed, and rebuild against the
geometry that was already working. A windowed failure still
propagates, since there is no smaller state to fall back to.
enricobuehler merged commit 3725db7cfe into main 2026-08-29 14:38:55 +00:00
enricobuehler deleted branch worktree-win-f11-swapchain 2026-08-29 14:39:06 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#443