Skip to content

window: drop a worker frame the window has resized past - #786

Merged
LinuxJedi merged 1 commit into
v1from
fix/present-worker-stale-surface-loop
Oct 17, 2026
Merged

LinuxJedi merged 1 commit into
v1from
fix/present-worker-stale-surface-loop

Conversation

@LinuxJedi

Copy link
Copy Markdown
Collaborator

Problem

A bug report: on Windows 11 with an AMD GPU, resizing the main window hangs Copperline, and the whole machine is very slow until it recovers. It reproduces reliably.

Since 0.21.0 the present worker plans each frame on the main thread and presents it later. On Windows the main thread pumps the modal size-move loop while the user drags an edge, so the window changes size while frames are still queued for the worker. On Windows wgpu picks Vulkan first, and the driver returns VK_ERROR_OUT_OF_DATE_KHR when the swapchain extent no longer matches the window. pixels 0.17.2 retries that acquire in a loop with no bound, reconfiguring from its cached surface size. Only the main thread can update that size, and only after reclaiming the GPU side. So the worker rebuilt the swapchain at the stale size forever, while the main thread waited in PresentWorker::reclaim to apply the Resized event. The window stopped responding, and the swapchain churn starved the GPU driver.

This is the #362 loop again (fullscreen toggles on X11), now on a thread the corrective event cannot reach. The redraw-time resync from #362 does not cover it, because the size check runs on the main thread and the acquire runs on the worker. Single-threaded present never hit this on Windows: there the window can only change size while that same thread pumps messages.

Fix

  • Each PresentJob records the surface size it was planned for. Before acquiring, the worker compares that size with the window's live size (worker_surface_current). If the window has changed size, the worker drops the job: it was drawn for the old geometry anyway. The Resized event behind the change reconfigures the surface on the main thread, and the next frame is planned for the new size. On macOS the worker must not call winit, and the Metal layer never reports a resized surface as out of date, so it presents as before.
  • A resize can still land between that check and the acquire. To cover it, pixels is patched to upstream's bounded retry (Bound the retry loop while acquiring the surface texture parasyte/pixels#462, merged but unreleased; the pinned revision is 0.17.2 plus that change and an examples-only clippy pass). pixels now gives up after one reconfigure and returns GetSurface(Outdated). Both present paths log that at debug level as a dropped frame; other present failures are still errors.
  • The patch is a revision-pinned [patch.crates-io] in the root, crates/copperline-player and fuzz workspaces (the ones that build pixels), with the source allowed in deny.toml. Remove all of them once a pixels release includes Add built-in General MIDI device #462. The lockfile changes only swap pixels' source line.
  • docs/internals/video.md describes the worker-side check and the bounded retry.

Testing

  • New unit tests: present_worker_drops_a_frame_the_window_has_resized_past and an_out_of_date_surface_is_a_dropped_frame_not_an_error.
  • cargo test --release, cargo clippy --all-targets and cargo fmt --check are clean on macOS.
  • Not yet run on Windows. The reporter's machine is the real test. Builds without this fix can avoid the hang with COPPERLINE_THREADED_PRESENT=0 or WGPU_BACKEND=dx12.

Resizing the main window on Windows could hang Copperline and slow the
whole host until the process was killed (reported on Win11 with an AMD
GPU).

The present worker plans each frame on the main thread and presents it
later. On Windows the main thread pumps the modal size-move loop while
the user drags an edge, so the window changes size under frames still
queued for the worker. wgpu picks Vulkan first there, and the driver
answers VK_ERROR_OUT_OF_DATE_KHR for a swapchain whose extent no longer
matches the window. pixels 0.17.2 retries that acquire in a loop with no
bound, reconfiguring from its cached surface size, which only the main
thread can update, and only after reclaiming the GPU side. The worker
rebuilt the swapchain at the stale size forever while the main thread
waited in PresentWorker::reclaim to apply the Resized event: the window
stopped responding, and the swapchain churn starved the GPU driver. This
is issue #362's loop again, now on a thread the corrective event cannot
reach. Single-threaded present never hit it on Windows, since the window
can only change size while that same thread pumps messages.

Each PresentJob now carries the surface size it was planned for, and the
worker compares it with the window's live size before acquiring. A job
whose window has moved on is dropped: it was drawn for the old geometry
anyway, and the Resized behind the change reconfigures the surface on the
main thread for the next frame. On macOS the worker must not call winit,
and the Metal layer never reports a resized surface out of date, so it
presents as before.

The race between that check and the acquire is closed by patching in
upstream's bounded retry (parasyte/pixels#462, unreleased): pixels gives
up after one reconfigure and returns GetSurface(Outdated). That error is
logged at debug as a dropped frame on both present paths; other present
failures stay errors. The patch is a revision-pinned [patch.crates-io] in
the root, player and fuzz workspaces, allowed in deny.toml, to be removed
once a pixels release includes #462.

COPPERLINE_THREADED_PRESENT=0 or WGPU_BACKEND=dx12 avoid the hang on
builds without this fix.
Copilot AI balanced review requested due to automatic review settings October 17, 2026 05:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The GPU-threading fix still needs resize validation on the affected Windows/Vulkan platform.

No open issues 🎉

What changed in this PR

Addresses resize-related presentation hangs by dropping stale worker frames and bounding surface-acquisition retries.

Changes:

  • Check live window size before worker presentation.
  • Pin the upstream pixels retry fix and log outdated surfaces as dropped frames.
  • Add regression tests and update presentation documentation.
File Description
src/​video/​window/​tests.rs Tests stale-frame detection and error classification.
src/​video/​window/​present.rs Adds size checks and present-error logging helpers.
src/​video/​window.rs Records planned size and skips stale worker jobs.
fuzz/​Cargo.toml Applies the pixels patch to fuzzing.
docs/​internals/​video.md Documents resize handling and bounded retries.
deny.toml Allows the upstream pixels Git source.
crates/​copperline-player/​Cargo.toml Applies the pixels patch to the player.
crates/​copperline-player/​Cargo.lock Pins the player's patched pixels source.
Cargo.toml Pins the upstream bounded-retry patch.
Cargo.lock Locks the patched pixels source.

Review effort: Balanced

@LinuxJedi
LinuxJedi merged commit 78935ad into v1 Oct 17, 2026
38 checks passed
@LinuxJedi
LinuxJedi deleted the fix/present-worker-stale-surface-loop branch October 17, 2026 06:32
LinuxJedi added a commit that referenced this pull request Oct 17, 2026
Forward-merge #786 (drop a present-worker frame the window has resized past), #787 (copperhf refuses OpenDevice on an empty unit with TDERR_BadUnitNum), #788 (--mouse-to-after steers in screenshot pixels, a field at a time), #789 (copperhf marks queued requests NT_MESSAGE so WaitIO waits for them) and #790 (copperhf NUL-terminates the mounter's BSTRs).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants