1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-07 05:58:28 -04:00

wallpaper: optimize blurred wallpaper layer

port 1.5
This commit is contained in:
bbedward
2026-07-17 11:40:57 -04:00
parent bdaa6bcc9d
commit 504c132f78
2 changed files with 391 additions and 236 deletions
+136 -236
View File
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common import qs.Common
@@ -57,60 +56,96 @@ Variants {
} }
property string source: SessionData.getMonitorWallpaper(modelData.name) || "" property string source: SessionData.getMonitorWallpaper(modelData.name) || ""
property bool isColorSource: source.startsWith("#") readonly property bool isColorSource: source.startsWith("#")
readonly property string displaySource: {
if (!source || isColorSource)
return "";
return source.startsWith("file://") ? source : encodeFileUrl(source);
}
property bool contentReady: false property bool contentReady: false
property bool surfaceBounce: false property bool surfaceBounce: false
Connections { // Live stack is captured into frozenLayer once stable, then unloaded; if the capture stalls the stack just stays live.
target: SessionData property bool liveActive: false
function onIsLightModeChanged() { property bool frozenValid: false
if (SessionData.perModeWallpaper) { property string _frozenSource: ""
var newSource = SessionData.getMonitorWallpaper(modelData.name) || ""; property bool loadFailed: false
if (newSource !== root.source) { property int _freezeWaitFrames: 0
root.source = newSource;
}
}
}
}
function getFillMode(modeName) { readonly property var backingWindow: Window.window
switch (modeName) { readonly property bool renderActive: !source || liveActive || _freezeWaitFrames > 0
case "Stretch": property int _settleFrames: 3
return Image.Stretch;
case "Fit": readonly property int maxTextureSize: 8192
case "PreserveAspectFit": readonly property int textureWidth: Math.min(modelData.width, maxTextureSize)
return Image.PreserveAspectFit; readonly property int textureHeight: Math.min(modelData.height, maxTextureSize)
case "Fill":
case "PreserveAspectCrop":
return Image.PreserveAspectCrop;
case "Tile":
return Image.Tile;
case "TileVertically":
return Image.TileVertically;
case "TileHorizontally":
return Image.TileHorizontally;
case "Pad":
return Image.Pad;
default:
return Image.PreserveAspectCrop;
}
}
Component.onCompleted: { Component.onCompleted: {
isInitialized = true; if (!displaySource) {
if (!source || isColorSource) {
contentReady = true; contentReady = true;
return;
} }
liveActive = true;
} }
property bool isInitialized: false onDisplaySourceChanged: {
property real transitionProgress: 0 invalidate();
readonly property bool transitioning: transitionAnimation.running loadFailed = false;
property bool effectActive: false _freezeWaitFrames = 0;
property bool useNextForEffect: false if (!displaySource) {
readonly property var backingWindow: Window.window liveActive = false;
readonly property bool renderActive: !source || effectActive || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading frozenValid = false;
property int _settleFrames: 3 _frozenSource = "";
contentReady = true;
return;
}
liveActive = true;
}
function regenerate() {
invalidate();
if (!displaySource)
return;
if (liveActive) {
scheduleFreeze();
return;
}
liveActive = true;
}
function handleDisplayable() {
contentReady = true;
if (liveLoader.item?.currentFailed) {
if (!frozenValid)
loadFailed = true;
liveActive = false;
}
invalidate();
}
function scheduleFreeze() {
if (!liveLoader.item?.stable)
return;
frozenLayer.scheduleUpdate();
_freezeWaitFrames = 3;
_settleFrames = 3;
// No wedge watchdog: an occluded surface may never produce frames, the freeze just waits
backingWindow?.update();
}
function completeFreeze() {
const live = liveLoader.item;
if (!live || !live.stable)
return;
frozenValid = true;
_frozenSource = displaySource;
liveActive = false;
log.info("froze blur layer for", modelData.name);
invalidate();
}
onTextureWidthChanged: regenerate()
onTextureHeightChanged: regenerate()
function invalidate() { function invalidate() {
_settleFrames = 3; _settleFrames = 3;
@@ -157,29 +192,35 @@ Variants {
root._settleFrames--; root._settleFrames--;
root._wedgeBounced = false; root._wedgeBounced = false;
wedgeWatchdog.stop(); wedgeWatchdog.stop();
if (root._freezeWaitFrames > 0 && --root._freezeWaitFrames === 0)
root.completeFreeze();
} }
function onVisibleChanged() { function onVisibleChanged() {
root.invalidate(); root.invalidate();
} }
function onWidthChanged() { function onWidthChanged() {
root.invalidate(); root.regenerate();
} }
function onHeightChanged() { function onHeightChanged() {
root.invalidate(); root.regenerate();
}
function onResourcesLost() {
root.frozenValid = false;
root.regenerate();
} }
} }
Connections { Connections {
target: Quickshell target: Quickshell
function onScreensChanged() { function onScreensChanged() {
root.invalidate(); root.regenerate();
} }
} }
Connections { Connections {
target: SettingsData target: SettingsData
function onWallpaperFillModeChanged() { function onWallpaperFillModeChanged() {
root.invalidate(); root.regenerate();
} }
function onEffectiveWallpaperBackgroundColorChanged() { function onEffectiveWallpaperBackgroundColorChanged() {
root.invalidate(); root.invalidate();
@@ -188,18 +229,26 @@ Variants {
Connections { Connections {
target: SessionData target: SessionData
function onIsLightModeChanged() {
if (SessionData.perModeWallpaper) {
var newSource = SessionData.getMonitorWallpaper(modelData.name) || "";
if (newSource !== root.source) {
root.source = newSource;
}
}
}
function onMonitorWallpaperFillModesChanged() { function onMonitorWallpaperFillModesChanged() {
root.invalidate(); root.regenerate();
} }
function onPerMonitorWallpaperChanged() { function onPerMonitorWallpaperChanged() {
root.invalidate(); root.regenerate();
} }
} }
// Theme changes repaint DankBackdrop but nothing else wakes the render loop // Theme changes repaint DankBackdrop but nothing else wakes the render loop
Connections { Connections {
target: Theme target: Theme
enabled: root.isColorSource || currentWallpaper.status === Image.Error enabled: root.isColorSource || root.loadFailed
function onPrimaryChanged() { function onPrimaryChanged() {
root.invalidate(); root.invalidate();
} }
@@ -217,89 +266,23 @@ Variants {
} }
} }
function handleTransitionLoadError(failedSource) { Connections {
log.warn("failed to load candidate wallpaper for", modelData.name + ":", failedSource); target: liveLoader.item
transitionDelayTimer.stop(); function onBecameDisplayable() {
transitionAnimation.stop(); root.handleDisplayable();
root.useNextForEffect = false;
root.effectActive = false;
root.transitionProgress = 0.0;
nextWallpaper.source = "";
}
onSourceChanged: {
invalidate();
if (!source || source.startsWith("#")) {
setWallpaperImmediate("");
return;
} }
function onStableChanged() {
const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source); if (liveLoader.item.stable)
root.scheduleFreeze();
if (!isInitialized || !currentWallpaper.source) {
setWallpaperImmediate(formattedSource);
isInitialized = true;
return;
} }
if (CompositorService.isNiri && SessionData.isSwitchingMode) { function onTransitioningChanged() {
setWallpaperImmediate(formattedSource); root.invalidate();
return;
} }
changeWallpaper(formattedSource);
}
function setWallpaperImmediate(newSource) {
transitionDelayTimer.stop();
transitionAnimation.stop();
root.transitionProgress = 0.0;
root.effectActive = false;
if (!newSource)
root.contentReady = true;
currentWallpaper.source = newSource;
nextWallpaper.source = "";
}
function startTransition() {
root.useNextForEffect = true;
root.effectActive = true;
if (srcNext.scheduleUpdate)
srcNext.scheduleUpdate();
transitionDelayTimer.start();
}
Timer {
id: transitionDelayTimer
interval: 16
repeat: false
onTriggered: transitionAnimation.start()
}
function changeWallpaper(newPath) {
if (newPath === currentWallpaper.source)
return;
if (!newPath || newPath.startsWith("#"))
return;
if (root.transitioning) {
transitionAnimation.stop();
root.transitionProgress = 0;
root.effectActive = false;
currentWallpaper.source = nextWallpaper.source;
nextWallpaper.source = "";
}
if (!currentWallpaper.source) {
setWallpaperImmediate(newPath);
return;
}
nextWallpaper.source = newPath;
if (nextWallpaper.status === Image.Ready)
root.startTransition();
} }
Loader { Loader {
anchors.fill: parent anchors.fill: parent
active: !root.source || root.isColorSource || currentWallpaper.status === Image.Error active: !root.source || root.isColorSource || root.loadFailed
asynchronous: true asynchronous: true
sourceComponent: DankBackdrop { sourceComponent: DankBackdrop {
@@ -307,126 +290,43 @@ Variants {
} }
} }
readonly property int maxTextureSize: 8192
property int textureWidth: Math.min(modelData.width, maxTextureSize)
property int textureHeight: Math.min(modelData.height, maxTextureSize)
Image {
id: currentWallpaper
anchors.fill: parent
visible: false
opacity: 1
asynchronous: true
retainWhileLoading: true
smooth: true
cache: true
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(modelData.name))
onStatusChanged: {
if (status === Image.Error) {
log.warn("failed to load active wallpaper for", modelData.name + ":", source);
}
if (status === Image.Ready || status === Image.Error) {
root.contentReady = true;
}
}
}
Image {
id: nextWallpaper
anchors.fill: parent
visible: false
opacity: 0
asynchronous: true
retainWhileLoading: true
smooth: true
cache: true
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(modelData.name))
onStatusChanged: {
if (status === Image.Error) {
root.handleTransitionLoadError(source);
return;
}
if (status !== Image.Ready)
return;
if (!root.transitioning) {
root.startTransition();
}
}
}
ShaderEffectSource { ShaderEffectSource {
id: srcNext id: frozenLayer
sourceItem: root.effectActive ? nextWallpaper : null anchors.fill: parent
hideSource: root.effectActive sourceItem: liveContainer
live: root.effectActive
mipmap: false
recursive: false
textureSize: Qt.size(root.textureWidth, root.textureHeight)
}
Rectangle {
id: dummyRect
width: 1
height: 1
visible: false
color: "transparent"
}
ShaderEffectSource {
id: srcDummy
sourceItem: dummyRect
hideSource: true
live: false live: false
mipmap: false mipmap: false
recursive: false recursive: false
smooth: true
visible: root.frozenValid || root.liveActive
textureSize: Qt.size(root.textureWidth, root.textureHeight)
} }
Item { Item {
id: blurredLayer id: liveContainer
anchors.fill: parent anchors.fill: parent
visible: root.liveActive
MultiEffect { Loader {
id: liveLoader
anchors.fill: parent anchors.fill: parent
source: currentWallpaper active: root.liveActive
visible: currentWallpaper.source !== "" asynchronous: false
blurEnabled: true
blur: 0.8
blurMax: 75
opacity: 1 - root.transitionProgress
autoPaddingEnabled: false
}
MultiEffect { // Cached images reach Ready synchronously during creation, before Connections retargets
anchors.fill: parent onLoaded: {
source: root.useNextForEffect ? srcNext : srcDummy if (item.displayableNow)
visible: nextWallpaper.source !== "" && root.useNextForEffect root.handleDisplayable();
blurEnabled: true if (item.stable)
blur: 0.8 root.scheduleFreeze();
blurMax: 75 }
opacity: root.transitionProgress
autoPaddingEnabled: false
}
}
NumberAnimation { sourceComponent: BlurredWallpaperLive {
id: transitionAnimation wallpaperSource: root.displaySource
target: root initialSource: root._frozenSource
property: "transitionProgress" screenName: modelData.name
from: 0.0 blurTextureSize: Qt.size(root.textureWidth, root.textureHeight)
to: 1.0 }
duration: 1000
easing.type: Easing.InOutCubic
onFinished: {
if (nextWallpaper.source && nextWallpaper.status === Image.Ready)
currentWallpaper.source = nextWallpaper.source;
root.useNextForEffect = false;
nextWallpaper.source = "";
root.transitionProgress = 0.0;
root.effectActive = false;
} }
} }
} }
+255
View File
@@ -0,0 +1,255 @@
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Services
Item {
id: root
readonly property var log: Log.scoped("BlurredWallpaperLive")
required property string wallpaperSource
required property string initialSource
required property string screenName
required property size blurTextureSize
readonly property bool currentFailed: currentWallpaper.status === Image.Error
readonly property bool displayableNow: currentWallpaper.status === Image.Ready || currentWallpaper.status === Image.Error
readonly property bool stable: isInitialized && !effectActive && !transitionAnimation.running && !transitionDelayTimer.running && currentWallpaper.status === Image.Ready && currentWallpaper.source.toString() === wallpaperSource && !nextWallpaper.source.toString()
signal becameDisplayable
property bool isInitialized: false
property real transitionProgress: 0
readonly property bool transitioning: transitionAnimation.running
property bool effectActive: false
property bool useNextForEffect: false
function getFillMode(modeName) {
switch (modeName) {
case "Stretch":
return Image.Stretch;
case "Fit":
case "PreserveAspectFit":
return Image.PreserveAspectFit;
case "Fill":
case "PreserveAspectCrop":
return Image.PreserveAspectCrop;
case "Tile":
return Image.Tile;
case "TileVertically":
return Image.TileVertically;
case "TileHorizontally":
return Image.TileHorizontally;
case "Pad":
return Image.Pad;
default:
return Image.PreserveAspectCrop;
}
}
Component.onCompleted: {
if (initialSource && initialSource !== wallpaperSource && !(CompositorService.isNiri && SessionData.isSwitchingMode)) {
currentWallpaper.source = initialSource;
isInitialized = true;
changeWallpaper(wallpaperSource);
return;
}
currentWallpaper.source = wallpaperSource;
isInitialized = true;
}
onWallpaperSourceChanged: {
if (!isInitialized)
return;
if (!wallpaperSource) {
setWallpaperImmediate("");
return;
}
if (!currentWallpaper.source.toString()) {
setWallpaperImmediate(wallpaperSource);
return;
}
if (CompositorService.isNiri && SessionData.isSwitchingMode) {
setWallpaperImmediate(wallpaperSource);
return;
}
changeWallpaper(wallpaperSource);
}
function handleTransitionLoadError(failedSource) {
log.warn("failed to load candidate wallpaper for", screenName + ":", failedSource);
transitionDelayTimer.stop();
transitionAnimation.stop();
useNextForEffect = false;
effectActive = false;
transitionProgress = 0.0;
nextWallpaper.source = "";
}
function setWallpaperImmediate(newSource) {
transitionDelayTimer.stop();
transitionAnimation.stop();
transitionProgress = 0.0;
effectActive = false;
currentWallpaper.source = newSource;
nextWallpaper.source = "";
}
function startTransition() {
useNextForEffect = true;
effectActive = true;
if (srcNext.scheduleUpdate)
srcNext.scheduleUpdate();
transitionDelayTimer.start();
}
function changeWallpaper(newPath) {
if (!newPath)
return;
if (newPath === currentWallpaper.source.toString())
return;
if (transitioning) {
transitionAnimation.stop();
transitionProgress = 0;
effectActive = false;
currentWallpaper.source = nextWallpaper.source;
nextWallpaper.source = "";
}
if (!currentWallpaper.source.toString()) {
setWallpaperImmediate(newPath);
return;
}
nextWallpaper.source = newPath;
if (nextWallpaper.status === Image.Ready)
startTransition();
}
Timer {
id: transitionDelayTimer
interval: 16
repeat: false
onTriggered: transitionAnimation.start()
}
Image {
id: currentWallpaper
anchors.fill: parent
visible: false
opacity: 1
asynchronous: true
retainWhileLoading: true
smooth: true
cache: true
sourceSize: root.blurTextureSize
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(root.screenName))
onStatusChanged: {
if (status === Image.Error) {
root.log.warn("failed to load active wallpaper for", root.screenName + ":", source);
}
if (status === Image.Ready || status === Image.Error) {
root.becameDisplayable();
}
}
}
Image {
id: nextWallpaper
anchors.fill: parent
visible: false
opacity: 0
asynchronous: true
retainWhileLoading: true
smooth: true
cache: true
sourceSize: root.blurTextureSize
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(root.screenName))
onStatusChanged: {
if (status === Image.Error) {
root.handleTransitionLoadError(source);
return;
}
if (status !== Image.Ready)
return;
if (!root.transitioning) {
root.startTransition();
}
}
}
ShaderEffectSource {
id: srcNext
sourceItem: root.effectActive ? nextWallpaper : null
hideSource: root.effectActive
live: root.effectActive
mipmap: false
recursive: false
textureSize: root.blurTextureSize
}
Rectangle {
id: dummyRect
width: 1
height: 1
visible: false
color: "transparent"
}
ShaderEffectSource {
id: srcDummy
sourceItem: dummyRect
hideSource: true
live: false
mipmap: false
recursive: false
}
Item {
id: blurredLayer
anchors.fill: parent
MultiEffect {
anchors.fill: parent
source: currentWallpaper
visible: currentWallpaper.source !== ""
blurEnabled: true
blur: 0.8
blurMax: 75
opacity: 1 - root.transitionProgress
autoPaddingEnabled: false
}
MultiEffect {
anchors.fill: parent
source: root.useNextForEffect ? srcNext : srcDummy
visible: nextWallpaper.source !== "" && root.useNextForEffect
blurEnabled: true
blur: 0.8
blurMax: 75
opacity: root.transitionProgress
autoPaddingEnabled: false
}
}
NumberAnimation {
id: transitionAnimation
target: root
property: "transitionProgress"
from: 0.0
to: 1.0
duration: 1000
easing.type: Easing.InOutCubic
onFinished: {
if (nextWallpaper.source && nextWallpaper.status === Image.Ready)
currentWallpaper.source = nextWallpaper.source;
root.useNextForEffect = false;
nextWallpaper.source = "";
root.transitionProgress = 0.0;
root.effectActive = false;
}
}
}