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:
1 parent
d3fb0284a8
commit
387fcf599a
4 files changed
+90
-3
No files matched your search
@@ -8,6 +8,9 @@ Item {
|
||||
|
||||
property string path: ""
|
||||
property int version: 0
|
||||
// Unversioned images cache by path. A caller whose version changes with every
|
||||
// new file, as the lock's does, may cache too: the version is in the URL.
|
||||
property bool cached: version === 0
|
||||
// A caller that knows the size its screen needs, in physical pixels, sets
|
||||
// constrainDecode and decodeSize. The image then waits for that size rather
|
||||
// than decoding at the file's native size first.
|
||||
@@ -35,7 +38,7 @@ Item {
|
||||
source: !root.constrainDecode || root.decodeSize.width > 0 ? root.imageUrl : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
cache: root.version === 0
|
||||
cache: root.cached
|
||||
sourceSize.width: root.constrainDecode ? root.decodeSize.width : (root.version > 0 ? width : 0)
|
||||
sourceSize.height: root.constrainDecode ? root.decodeSize.height : (root.version > 0 ? height : 0)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
run_node_test <<'JS'
|
||||
const fs = require('fs')
|
||||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
|
||||
const media = read('shell/Ui/BackgroundMedia.qml')
|
||||
const view = read('shell/plugins/lock/LockView.qml')
|
||||
const service = read('shell/plugins/lock/Service.qml')
|
||||
|
||||
// The lock draws its wallpaper on the first frame only if the image it asks
|
||||
// for is already in the cache: same URL, same requested size, same fill mode.
|
||||
assert(
|
||||
media.includes('property bool cached: version === 0') && media.includes('cache: root.cached'),
|
||||
'background media caches unversioned images, and versioned ones on request'
|
||||
)
|
||||
assert(
|
||||
/version: root\.backgroundVersion\s*(\/\/.*\n\s*)*cached: true\s*constrainDecode: true\s*decodeSize: Qt\.size\(width, height\)/.test(view),
|
||||
'the lock wallpaper waits for its size and reads from the cache'
|
||||
)
|
||||
assert(
|
||||
service.includes('readonly property string lockWallpaperPath: videoBackground ? videoPosterPath : backgroundPath') &&
|
||||
service.includes('? Util.fileUrl(lockWallpaperPath) + (backgroundVersion ? "?v=" + backgroundVersion : "")') &&
|
||||
media.includes('Util.fileUrl(path) + (version ? "?v=" + version : "")'),
|
||||
'the lock service preloads the exact URL the lock view requests'
|
||||
)
|
||||
assert(
|
||||
/model: Quickshell\.screens\s*Image \{\s*required property var modelData\s*visible: false\s*source: root\.lockWallpaperUrl\s*sourceSize\.width: modelData\.width\s*sourceSize\.height: modelData\.height\s*fillMode: Image\.PreserveAspectCrop\s*asynchronous: true\s*cache: true/.test(service),
|
||||
'the lock service keeps each screen\'s lock wallpaper decoded at the lock\'s size and fill'
|
||||
)
|
||||
|
||||
// A wallpaper overwritten in place keeps its path, so the version, which is
|
||||
// part of the cached URL, follows the file's mtime and size as well.
|
||||
assert(
|
||||
service.includes('stat -Lc %Y:%s') &&
|
||||
/\} else if \(signature !== root\.backgroundSignature\) \{\s*root\.backgroundSignature = signature\s*root\.backgroundVersion \+= 1/.test(service),
|
||||
'an overwritten wallpaper bumps the lock wallpaper version'
|
||||
)
|
||||
JS
|
||||
Reference in new issue
Block a user