Skip to content

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

Open
Makoi66 wants to merge 2 commits into
CaptSilver:mainfrom
Makoi66:perf/mpv-switch-freezes
Open

Makoi66 wants to merge 2 commits into
CaptSilver:mainfrom
Makoi66:perf/mpv-switch-freezes

Conversation

@Makoi66

@Makoi66 Makoi66 commented Oct 2, 2026 •

Copy link
Copy Markdown

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.

Switching wallpapers faded the current backend out at once and created
the next one underneath.  A cold scene start takes seconds (shader
compiles), so the desktop showed the bare background colour, with the
icons over it, for all of that; scene->scene and video->video swaps
did not even fade, since they reassigned `source` on the live item.

Each backend now exposes `frameShown`, set on its first frame
(InfoShow, a static pane, starts true).  backendLoader.load() keeps the
backend that is on screen as `_outgoing`, stacked above and still
playing, until the new one sets frameShown; then it fades out over
250 ms (an instant cut with "Reduce animations") and is destroyed.  A
backend that never reached the screen during rapid switching is
dropped at once, so what the user sees stays up rather than a blank
gap.  A 25 s timeout guarantees the old layer never lingers; the
backends' own load watchdogs still swap in InfoShow on failure.

A different wallpaper now always gets a fresh backend, even of the same
type, so it goes through this handoff.  A path change on the same
workshop id (another page of the same web wallpaper) still swaps in
place, as before; folder videos carry no id, so for them any path
change is a new wallpaper.

The outgoing backend must also stop following background.*, which
describes the next wallpaper from the moment the switch starts:
WallpaperWorkShopId changes first and read_wallpaper_config resolves
synchronously, so the old wallpaper immediately took the new one's
display mode, speed and user properties and stretched or cropped until
the swap finished.  Backends gain freezeOptions(), which pins those to
their current values; load() calls it on the outgoing backend, and the
new wallpaper's options are read after that (readCurrentOptions, before
`source` is set, so USER_PROPS still precede LOAD_SCENE).  The
workshopid handler defers its read with Qt.callLater for the same
reason.

tst_main_handoff.qml drives this through the real main.qml: the old
scene stays on top until the new one draws and then fades, the
outgoing scene keeps its own display mode, a rapid A->B->C switch keeps
A on screen, and a same-id path change does not rebuild.
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).
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.

1 participant