Skip to content

Stop the video-switch freeze: async mpv stop, no sync reads per event - #36

Merged
CaptSilver merged 5 commits into
CaptSilver:mainfrom
Makoi66:perf/mpv-switch-freezes
Oct 13, 2026
Merged

CaptSilver merged 5 commits into
CaptSilver:mainfrom
Makoi66:perf/mpv-switch-freezes

Conversation

@Makoi66

@Makoi66 Makoi66 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #35 (the handoff's fade-out is what calls prepareDestroy). Until that one is merged, this PR's diff includes its commit.

Problem

Switching away from a video wallpaper froze every plasmashell window for ~200 ms (QSG_RENDER_TIMING, on a build based on an older main, 5c3d184; the teardown code below is the same on today's main). There are two synchronous waits on the mpv core:

  1. ~MpvRender calls mpv_render_context_free on the render thread during the sync (GUI thread blocked). That call waits for the core to tear down its video output.
  2. onMpvEvents answered every observed-property event with two mpv_get_property calls on the GUI thread. Those block while the core is busy, which is exactly the case during a teardown.

Change

  • onMpvEvents takes idle-active/pause from the MPV_EVENT_PROPERTY_CHANGE payload (both are observed as MPV_FORMAT_FLAG) and keeps the last pair.
  • MpvObject::stopAsync() does two things: it detaches the owner (MpvHandle::detachOwner, the first half of beginShutdown) and issues an async stop.
  • Mpv.qml exposes it as prepareDestroy(). The fade-out calls that, then destroys the backend 400 ms later, when mpv_render_context_free no longer waits.

Tests

tst_mpvbackend checks that:

  • stopAsync doesn't wait on the core;
  • an observed change after it no longer reaches the object (a control run shows the same change does without it);
  • repeated and uninitialised calls are safe.

tools/scripts/preflight.sh passes (run inside a fedora-toolbox:latest container built from DEPS_FEDORA, WEK_IN_CI=1). One note: on this laptop the fuzz step times out on the checked-in seed tests/fuzz_corpus/WPShaderCompile/seed/58392d50… (~33 s per compile under ASAN+UBSAN, identical on clean main), so I ran preflight with --no-fuzz and then all fuzz targets with the same settings except -timeout=60: no findings.

Setup: Plasma 6 (Wayland), KWin, Intel Iris Xe (RPL-P), 2520x1680 @ 120 Hz laptop panel, Manjaro.

Makoi66 and others added 4 commits September 30, 2026 15:19
Switching away from a video wallpaper froze every plasmashell window
for ~200 ms (QSG_RENDER_TIMING).  Two synchronous waits on the mpv core:

- ~MpvRender runs mpv_render_context_free on the Qt render thread
  during the scenegraph sync (GUI thread blocked), and that call waits
  for the core to tear its video output down.
- onMpvEvents answered every observed-property event with two
  mpv_get_property calls ("idle-active", "pause") on the GUI thread,
  which block while the core is busy, i.e. exactly during a teardown.

onMpvEvents now takes both values from the MPV_EVENT_PROPERTY_CHANGE
payload (both are observed as MPV_FORMAT_FLAG) and keeps the last seen
pair, so an event never calls back into the core.

MpvObject::stopAsync() detaches the owner from mpv's wakeup callback
(MpvHandle::detachOwner, the first half of beginShutdown) and issues an
async "stop".  Mpv.qml exposes it as prepareDestroy(), and the handoff's
fade-out calls that on the outgoing backend and destroys it 400 ms
later: by then mpv has dropped its video output, and
mpv_render_context_free no longer waits.  Backends without
prepareDestroy are destroyed right away, as before.

tst_mpvbackend covers stopAsync: it returns without waiting on the
core, an observed-property change after it no longer reaches the
object (with a control run showing the same change does without it),
and repeated / uninitialised calls are safe.

Stacked on "Keep the old wallpaper on screen until the new one has
drawn" (the fade-out that calls prepareDestroy).
The async mpv stop (Mpv.qml prepareDestroy) was only called from
outgoingFade.onFinished in main.qml. The other places that destroy a
backend still did it synchronously and brought back the render-thread
stall on mpv_render_context_free.

- main.qml: add backendLoader.retire(backend), which calls
  prepareDestroy() when the backend has one, then destroys it (delayed
  400 ms in that case). outgoingFade.onFinished, Component.onDestruction,
  a fade cut short by dropOutgoing(), a backend that never drew, and an
  outgoing backend being replaced all go through it.
- tests/qml/_stubs Mpv.qml: add stopAsync() and a stopAsyncCount
  recorder; prepareDestroy() calls it and would throw on the stub.
  Mpv row in the stubs README updated.
- tst_main_handoff.qml: one test per destroy path, using a backend
  stand-in that records prepareDestroy() calls.
- tst_backend_mpv.qml: prepareDestroy() reaches the player's stopAsync().
@CaptSilver
CaptSilver merged commit 14a650e into CaptSilver:main Oct 13, 2026
5 checks passed
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