Keep the lock wallpaper decoded so waking shows it with the password field (#13431)

The lock started decoding its wallpaper only once locked, with the cache
off, and first at the view's unsized native resolution. A machine
suspending right after locking froze that decode partway, so waking
showed the password field on a bare background and the wallpaper
popped in after it. On this machine the wallpaper took ~208ms to become
ready, and the suspend followed the lock by 66ms.

The lock service now keeps each screen's lock wallpaper decoded in the
image cache, as the lock view requests it: same URL, the screen's
logical size, PreserveAspectCrop. The view waits for its size and reads
from the cache, so the wallpaper is ready within ~3ms of the lock
starting. The version in the cached URL follows the file's mtime and
size, so a wallpaper overwritten in place still reloads.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier HanssonandClaude Opus 5.5 authored and GitHub committed 2026-09-27 20:00:08 +02:00
1 parent d3fb0284a8
commit 387fcf599a
4 files changed
+90 -3

No files matched your search

+7
View File
@@ -96,6 +96,13 @@ Item {
anchors.fill: parent
path: root.loadBackground ? (root.video ? root.videoPosterPath : root.backgroundPath) : ""
version: root.backgroundVersion
// Decode only once sized, at the lock's own size: an unsized first
// request decoded the file at its native resolution, then again once
// sized. That size is what the lock service keeps decoded ahead of the
// lock, so the first frame has the wallpaper.
cached: true
constrainDecode: true
decodeSize: Qt.size(width, height)
}
MultiEffect {
+37 -2
View File
@@ -30,6 +30,9 @@ Item {
property string backgroundPath: ""
property string videoPosterPath: ""
property int backgroundVersion: 0
// The wallpaper file's mtime and size. The lock caches its wallpaper by
// version, so a file overwritten in place must bump the version too.
property string backgroundSignature: ""
property string lastEvent: "init"
property string lastEventAt: ""
property bool displaysBlank: false
@@ -407,6 +410,32 @@ Item {
}
}
// The lock only starts decoding its wallpaper once locked, and a machine
// suspending right after locking froze that decode partway: waking showed
// the password field on a bare background, then the wallpaper popped in.
// Keep each screen's lock wallpaper decoded in the image cache ahead of
// time, as the lock view requests it (same URL, the screen's logical size,
// PreserveAspectCrop), so the lock draws it on its first frame.
readonly property string lockWallpaperPath: videoBackground ? videoPosterPath : backgroundPath
readonly property string lockWallpaperUrl: lockWallpaperPath && !Util.isVideoPath(lockWallpaperPath)
? Util.fileUrl(lockWallpaperPath) + (backgroundVersion ? "?v=" + backgroundVersion : "")
: ""
Variants {
model: Quickshell.screens
Image {
required property var modelData
visible: false
source: root.lockWallpaperUrl
sourceSize.width: modelData.width
sourceSize.height: modelData.height
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
}
}
Timer {
id: fingerprintRetryTimer
interval: 250
@@ -416,14 +445,20 @@ Item {
Process {
id: readlinkProc
command: ["readlink", "-f", root.currentBackgroundLink]
command: ["bash", "-c", "path=$(readlink -f -- \"$1\") && printf '%s\\n%s\\n' \"$path\" \"$(stat -Lc %Y:%s -- \"$path\" 2>/dev/null)\"", "_", root.currentBackgroundLink]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
var next = String(text || "").trim()
var lines = String(text || "").split("\n")
var next = String(lines[0] || "").trim()
var signature = String(lines[1] || "").trim()
if (next !== root.backgroundPath) {
root.videoPosterPath = ""
root.backgroundPath = next
root.backgroundSignature = signature
root.backgroundVersion += 1
} else if (signature !== root.backgroundSignature) {
root.backgroundSignature = signature
root.backgroundVersion += 1
}
root.refreshPoster()