Decode theme backgrounds earlier, and at screen size (#13408)

* Decode the next theme background while the theme stages

The wipe waited 125-290ms after the transition arrived, decoding the new
wallpaper. Most stock wallpapers are WebP, which Qt decodes at full size
and scales afterwards, so a screen-sized sourceSize does not shorten it.

Start the decode earlier instead. omarchy-theme-set chooses the next
background and snapshots it before rendering templates, then sends a new
background prepare call in the background. The shell loads it into the
hidden incoming frame, so the transition finds it decoded. A prepare that
arrives after its transition is ignored, and one no transition claims is
dropped after five seconds.

The wipe now starts ~255ms after omarchy-theme-set begins instead of
~345-490ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Decode the wallpaper at screen size instead of shipped size

Wallpapers decoded at the resolution they were shipped at: a 5120x2880
stock wallpaper took 5120x2880 of RGBA on a 1920x1200 panel, and a
transition held up to three such frames. Bind sourceSize on the
displayed wallpaper and both transition frames to the screen's physical
size. PreserveAspectCrop treats it as the area to cover, so the image
still fills the screen.

Qt scales a decode up as well as down to cover sourceSize, so the native
size is read from the file header first with magick identify, and a
wallpaper smaller than the screen decodes at its own size. The images
wait for both sizes, so nothing decodes at native size first.

Ported from #8324 onto BackgroundMedia and the prepared incoming frame.
Measured with a 5120x2880 wallpaper, the shell's GPU memory at rest
drops from 264 MiB to ~148 MiB. The size probe delays the reveal by
~30ms, which the earlier prepare still more than covers.

Co-authored-by: Ryan Yogan <ryanyogan@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Ryan Yogan <ryanyogan@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-09-27 16:11:36 +02:00
1 parent 7e8d35efb3
commit f3dbc464fc
4 files changed
+209 -10

No files matched your search

+55
View File
@@ -19,4 +19,59 @@ assert(
!backgroundQml.includes('pendingThemeVersion !== backgroundVersion'),
'background theme transition applies pending colors even if image reveal stalls'
)
const themeSet = fs.readFileSync(path.join(root, 'bin/omarchy-theme-set'), 'utf8')
// The next background decodes while the theme stages, rather than after the
// transition arrives: WebP decodes take as long at screen size as at native.
assert(
/function prepare\(path: string\): void \{\s*root\.prepareBackground\(path\)/.test(backgroundQml) &&
backgroundQml.includes('readonly property string framePath: root.incomingBackground || root.preparedBackground'),
'background decodes a prepared theme background in the hidden incoming frame'
)
assert(
/path === lastTransitionPath/.test(backgroundQml) &&
/id: preparedBackgroundTimer[\s\S]*?onTriggered: root\.preparedBackground = ""/.test(backgroundQml),
'background ignores a late prepare and drops an unclaimed one'
)
assert(
themeSet.indexOf('shell_ipc background prepare') !== -1 &&
themeSet.indexOf('shell_ipc background prepare') < themeSet.indexOf('\nomarchy-theme-set-templates\n'),
'theme set hands the shell its next background before rendering templates'
)
assert(
themeSet.includes('shell_ipc background prepare "$PREPARED_BACKGROUND_SNAPSHOT" 9>&- &'),
'theme set sends the prepare without holding the theme lock or waiting on it'
)
// The wallpaper is decoded at the screen's physical size, never at the size
// it was shipped at, unless it is smaller than the screen: then it is decoded
// at its own size instead of being scaled up to cover the screen.
const mediaQml = fs.readFileSync(path.join(root, 'shell/Ui/BackgroundMedia.qml'), 'utf8')
assert(
backgroundQml.includes('readonly property bool sized: width > 0 && height > 0') &&
backgroundQml.includes('readonly property int decodeWidth: sized ? Math.ceil(width * screen.devicePixelRatio) : 0') &&
backgroundQml.includes('readonly property int decodeHeight: sized ? Math.ceil(height * screen.devicePixelRatio) : 0'),
'background derives its decode size from the screen in physical pixels'
)
assert(
backgroundQml.includes('["magick", "identify", "-ping", "-format", "%w %h", sizeProbe.path]') &&
backgroundQml.includes('if (native.width > 0 && (native.width < decodeWidth || native.height < decodeHeight)) return Qt.size(native.width, native.height)'),
'background reads the wallpaper header and never decodes larger than the native size'
)
const count = (needle) => backgroundQml.split(needle).length - 1
assertEqual(count('sourceSize.width: decode.width'), 2, 'both transition frames bind their decode width')
assertEqual(count('sourceSize.height: decode.height'), 2, 'both transition frames bind their decode height')
assertEqual(count('source: decode.width > 0 ? root.imageUrl('), 2, 'both transition frames wait for the screen and native sizes before loading')
assert(
/constrainDecode: true\s*decodeSize: panel\.decodeSize\(root\.displayedBackground\)/.test(backgroundQml) &&
mediaQml.includes('source: !root.constrainDecode || root.decodeSize.width > 0 ? root.imageUrl : ""') &&
mediaQml.includes('sourceSize.width: root.constrainDecode ? root.decodeSize.width : (root.version > 0 ? width : 0)'),
'the displayed wallpaper waits for and decodes at the same size'
)
assert(
/function requestNativeSize\(path\) \{\s*if \(!path \|\| isVideo\(path\)/.test(backgroundQml) &&
/function prepareBackground[\s\S]*?requestNativeSize\(path\)/.test(backgroundQml),
'background never probes videos and probes a prepared frame ahead of its transition'
)
JS