Skip to content

Keep the old wallpaper on screen until the new one has drawn - #35

Merged
CaptSilver merged 2 commits into
CaptSilver:mainfrom
Makoi66:fix/wallpaper-handoff
Oct 13, 2026
Merged

CaptSilver merged 2 commits into
CaptSilver:mainfrom
Makoi66:fix/wallpaper-handoff

Conversation

@Makoi66

@Makoi66 Makoi66 commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Problem

On a switch the current backend faded out at once. A cold scene start takes seconds (shader compiles), so the bare background colour showed, with the desktop icons over it. scene->scene and video->video swaps even hard-cut, because they reassigned source on the live item.

The outgoing backend also followed 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 stretched or cropped to the new one's display mode until the swap finished. This is visible when only one of the two wallpapers has a per-wallpaper display_mode.

Change

  • frameShown. Each backend exposes it, set on its first frame. InfoShow starts at true.
  • Outgoing backend. backendLoader.load() keeps the backend 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.
  • Rapid switching. A backend that never reached the screen is dropped at once, so the one being viewed stays up.
  • Timeout. A 25 s timeout guarantees the old layer never lingers. The backends' own watchdogs still swap in InfoShow on failure.
  • When a fresh backend is created. A different wallpaper always gets one, even of the same type. A path change on the same workshop id still swaps in place, as before (see test_web_to_web_switch_mounts_fresh_backend). Folder videos have no id, so for them any path change counts as a new wallpaper.
  • freezeOptions(). Backends gain this; it pins display mode, speed and user props, and load() calls it on the outgoing backend. The new wallpaper's options are read after that, and still before source, so USER_PROPS precede LOAD_SCENE. onWorkshopidChanged defers its read with Qt.callLater.

Tests

tst_main_handoff.qml drives the real main.qml through ScreenRig and checks that:

  • the old scene stays on top until the new one draws, then fades;
  • the outgoing scene keeps its own display mode;
  • a rapid A->B->C switch keeps A on screen;
  • a same-id path change does not rebuild.

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.
…nt colour

During a wallpaper handoff the old backend stays on screen while
background.* already describes the next wallpaper. freezeOptions() only
pinned fill mode, speed and user properties, so the old layer still
followed the next wallpaper in three ways:

- Scene.qml: pin postprocessingOverride. A change reloaded the outgoing
  scene while it was the visible layer.
- Scene.qml, Mpv.qml, QtMultimedia.qml, QtWebView.qml: pin the mute flag,
  and (Scene, Mpv, QtMultimedia) the volume. A wallpaper muted per
  wallpaper used to unmute and play over the new one. Volume lives in a
  closure inside Common.createVolumeFade, so the fade handle gets a
  freeze() that drops the binding on its timer.
- main.qml: wallpaper.accentColorChanged() fired from the new backend's
  first-frame handler while the old layer was still opaque on top, so
  Plasma sampled the old wallpaper. It now waits for the outgoing fade to
  finish (emitAccentColor, called from outgoingFade.onFinished) and still
  fires at once when nothing is on top.

Tests: tst_main_handoff.qml covers postprocessing, mute, volume and both
accent-colour cases through the real main.qml; the Mpv, QtMultimedia and
QtWebView backend tests each check that freezeOptions() keeps mute.
@CaptSilver
CaptSilver merged commit da18efc into CaptSilver:main Oct 13, 2026
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