From e655ec5bc7aaa5a2406998d5c538ecb73f2151ba Mon Sep 17 00:00:00 2001 From: zach Date: Mon, 26 Oct 2026 14:47:47 +0100 Subject: [PATCH] feat: add wallpaper transitions, store crop in database per-wallpaper, move CUDA context creation to async thread --- Components/Menu.qml | 8 +- Helpers/Wallpapers.qml | 102 +- Modules/Settings/Common/WallpaperCropper.qml | 6 + Modules/Settings/Pages/WallpaperPage.qml | 48 + Plugins/ZShell/Config/background.hpp | 7 +- Plugins/ZShell/Config/enums.hpp | 13 + Plugins/ZShell/Internal/CMakeLists.txt | 11 + .../shaders/wallpaper_trans_bowtie.frag | 106 ++ .../shaders/wallpaper_trans_doom_melt.frag | 72 + .../shaders/wallpaper_trans_fade.frag | 39 + .../shaders/wallpaper_trans_hexagonalize.frag | 101 ++ .../shaders/wallpaper_trans_linear_blur.frag | 58 + ...paper_trans_split_slide_in_horizontal.frag | 49 + .../shaders/wallpaper_trans_tiles_wave.frag | 89 ++ .../wallpaper_trans_window_slices.frag | 42 + .../shaders/wallpaper_trans_wipe_down.frag | 34 + .../shaders/wallpaper_trans_wipe_right.frag | 34 + Plugins/ZShell/Internal/videoplayer.cpp | 42 +- Plugins/ZShell/Internal/videoplayer.hpp | 3 +- Plugins/ZShell/Internal/wallpaperobject.cpp | 40 +- Plugins/ZShell/Internal/wallpaperobject.hpp | 4 + Plugins/ZShell/Internal/wallpaperservice.cpp | 1282 +++++++++++++++-- Plugins/ZShell/Internal/wallpaperservice.hpp | 131 +- Plugins/ZShell/Models/wallpaperdb.cpp | 126 +- Plugins/ZShell/Models/wallpaperdb.hpp | 16 +- Plugins/ZShell/Models/wallpaperutil.hpp | 85 ++ 26 files changed, 2338 insertions(+), 210 deletions(-) create mode 100644 Plugins/ZShell/Internal/shaders/wallpaper_trans_bowtie.frag create mode 100644 Plugins/ZShell/Internal/shaders/wallpaper_trans_doom_melt.frag create mode 100644 Plugins/ZShell/Internal/shaders/wallpaper_trans_fade.frag create mode 100644 Plugins/ZShell/Internal/shaders/wallpaper_trans_hexagonalize.frag create mode 100644 Plugins/ZShell/Internal/shaders/wallpaper_trans_linear_blur.frag create mode 100644 Plugins/ZShell/Internal/shaders/wallpaper_trans_split_slide_in_horizontal.frag create mode 100644 Plugins/ZShell/Internal/shaders/wallpaper_trans_tiles_wave.frag create mode 100644 Plugins/ZShell/Internal/shaders/wallpaper_trans_window_slices.frag create mode 100644 Plugins/ZShell/Internal/shaders/wallpaper_trans_wipe_down.frag create mode 100644 Plugins/ZShell/Internal/shaders/wallpaper_trans_wipe_right.frag diff --git a/Components/Menu.qml b/Components/Menu.qml index 6a3db28..441b4eb 100644 --- a/Components/Menu.qml +++ b/Components/Menu.qml @@ -34,7 +34,7 @@ MouseArea { cursorShape: expanded ? Qt.ArrowCursor : undefined enabled: expanded hoverEnabled: expanded - layer.enabled: opacity < 1 + layer.enabled: false opacity: expanded ? 1 : 0 parent: { const win = QsWindow.window; @@ -101,8 +101,7 @@ MouseArea { yScale: root.expanded ? 1 : 0.1 Behavior on yScale { - Anim { - } + Anim {} } } @@ -137,8 +136,7 @@ MouseArea { radius: menu.radius - column.anchors.margins Behavior on radius { - Anim { - } + Anim {} } StateLayer { diff --git a/Helpers/Wallpapers.qml b/Helpers/Wallpapers.qml index 272e106..e8226ae 100644 --- a/Helpers/Wallpapers.qml +++ b/Helpers/Wallpapers.qml @@ -16,16 +16,15 @@ Searcher { id: root property string actualCurrent: WallpaperPath.currentWallpaperPath - property alias crops: adapter.monitorCrops readonly property string current: showPreview ? previewPath : actualCurrent readonly property string currentThumbnail: thumbnailFor(current) readonly property bool currentIsVideo: isVideoPath(current) - property alias monitorCrops: monitorCrops property bool pendingPreviewClear property bool previewColorLock property string previewPath property bool recentlyChanged property bool showPreview: false + property int cropVersion: 0 function getCategoryFor(w: WallpaperEntry): string { let category = w.parentDir.slice(Paths.wallsdir.length + 1); @@ -58,7 +57,7 @@ Searcher { } function getCrop(screen: string): var { - return root.crops[screen]; + return wallpapers.crop(root.current, screen); } function preview(path: string): void { @@ -75,19 +74,18 @@ Searcher { else if (zoom > 5.0) zoom = 5.0; - root.crops[screen] = { + wallpapers.setCrop(root.current, screen, { x: rect.x, y: rect.y, width: rect.width, height: rect.height, zoom: zoom - }; + }); } function setWallpaper(path: string): void { actualCurrent = path; WallpaperPath.currentWallpaperPath = path; - Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), 1.0)); Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]); if (Config.colors.schemeGen) Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]); @@ -121,32 +119,6 @@ Searcher { target: "wallpaper" } - FileView { - id: monitorCrops - - path: `${Paths.state}/wallpaper-crops.json` - watchChanges: true - - onAdapterUpdated: cropWriteDelay.restart() - onFileChanged: reload() - - JsonAdapter { - id: adapter - - property var monitorCrops: ({}) - } - } - - Timer { - id: cropWriteDelay - - interval: 100 - repeat: false - running: false - - onTriggered: monitorCrops.writeAdapter() - } - WallpaperDb { id: wallpapers @@ -183,35 +155,89 @@ Searcher { if (path === root.current) wallpaper.invalidateCache(); } + + function onCropsChanged(path: string): void { + if (path === root.current) + root.cropVersion++; + } } Variants { + id: screenVariants model: Quickshell.screens WallpaperObject { - readonly property var crops: root.getCrop(modelData.name) + id: obj required property var modelData - cropHeight: crops?.height ?? 1.0 - cropWidth: crops?.width ?? 1.0 - cropX: crops?.x ?? 0.0 - cropY: crops?.y ?? 0.0 + // Crop animation only for edits, never for wallpaper swaps. + property bool animateCrop: false + screen: modelData.name service: wallpaper - zoom: crops?.zoom ?? 1.0 + + // Apply the wallpaper's own crop. Driven off the service's pathChanged + // (which fires after applyPath has snapshotted the outgoing frame with + // the outgoing crop) and off crop edits, never as a reactive binding on + // the wallpaper path — so a swap applies the incoming crop atomically + // after the base is captured. + function seedCrop(): void { + // Load the current wallpaper's crop without pushing: the service + // already read it from the database. This only seeds the values so + // a later edit animates from the right starting point. + obj.syncCrop(root.getCrop(modelData.name)); + } + + // Apply an edited crop: assign via the setters so the Behaviors animate + // the numbers and push each intermediate value to the service. + function editCrop(): void { + const c = root.getCrop(modelData.name); + obj.cropX = c?.x ?? 0.0; + obj.cropY = c?.y ?? 0.0; + obj.cropWidth = c?.width ?? 1.0; + obj.cropHeight = c?.height ?? 1.0; + obj.zoom = c?.zoom ?? 1.0; + } + + Component.onCompleted: seedCrop() + onScreenChanged: seedCrop() Behavior on cropHeight { + enabled: obj.animateCrop Anim {} } Behavior on cropWidth { + enabled: obj.animateCrop Anim {} } Behavior on cropX { + enabled: obj.animateCrop Anim {} } Behavior on cropY { + enabled: obj.animateCrop Anim {} } } } + + Connections { + target: wallpaper + function onPathChanged(): void { + for (const obj of screenVariants.instances) { + obj.animateCrop = false; + obj.seedCrop(); + } + } + } + + Connections { + target: root + function onCropVersionChanged(): void { + for (const obj of screenVariants.instances) { + obj.animateCrop = true; + obj.editCrop(); + } + } + } } diff --git a/Modules/Settings/Common/WallpaperCropper.qml b/Modules/Settings/Common/WallpaperCropper.qml index 0c10a1b..aa0c5b6 100644 --- a/Modules/Settings/Common/WallpaperCropper.qml +++ b/Modules/Settings/Common/WallpaperCropper.qml @@ -14,10 +14,13 @@ Item { id: wrapper readonly property bool isVideo: Wallpapers.currentIsVideo + readonly property string currentWallpaper: Wallpapers.current readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null readonly property real maxZoom: 5.0 readonly property bool changesMade: { cropRevision; + Wallpapers.cropVersion; + wrapper.currentWallpaper; const r = cropRectLoader.item; if (!r || !currentScreen || scaledImg.status !== Image.Ready) @@ -115,6 +118,9 @@ Item { refreshScreens(); Qt.callLater(syncCropToScreen); } + onCurrentWallpaperChanged: { + Qt.callLater(syncCropToScreen); + } onSelectedScreenIndexChanged: { Qt.callLater(syncCropToScreen); } diff --git a/Modules/Settings/Pages/WallpaperPage.qml b/Modules/Settings/Pages/WallpaperPage.qml index f3fcb6e..fe9747f 100644 --- a/Modules/Settings/Pages/WallpaperPage.qml +++ b/Modules/Settings/Pages/WallpaperPage.qml @@ -13,6 +13,42 @@ import qs.Services PageBase { id: root + readonly property list wallTransitions: [ + MenuItem { + text: "Bowtie" + }, + MenuItem { + text: "Fade" + }, + MenuItem { + text: "LinearBlur" + }, + MenuItem { + text: "WindowSlices" + }, + MenuItem { + text: "WipeRight" + }, + MenuItem { + text: "WipeDown" + }, + MenuItem { + text: "TilesWave" + }, + MenuItem { + text: "SplitSlideInHorizontal" + }, + MenuItem { + text: "Hexagonalize" + }, + MenuItem { + text: "DoomMelt" + }, + MenuItem { + text: "Random" + } + ] + title: qsTr("Wallpaper & style") ColumnLayout { @@ -73,6 +109,18 @@ PageBase { onToggled: Config.background.enabled = checked } + SelectRow { + Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing + active: root.wallTransitions[Config.background.wallTransition] + menuItems: root.wallTransitions + menuOnTop: true + settingAnchor: "style-wallpaper-transition" + subtext: qsTr("Currently selected: %1").arg(active.text) + text: qsTr("Wallpaper transition") + + onSelected: item => Config.background.wallTransition = root.wallTransitions.indexOf(item) + } + ToggleRow { Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing checked: Colors.transparency.enabled diff --git a/Plugins/ZShell/Config/background.hpp b/Plugins/ZShell/Config/background.hpp index d079a2a..1ef73f1 100644 --- a/Plugins/ZShell/Config/background.hpp +++ b/Plugins/ZShell/Config/background.hpp @@ -1,7 +1,10 @@ #pragma once -#include "common.hpp" + #include +#include "common.hpp" +#include "enums.hpp" + namespace ZShell::config { class Background : public settings::ObjectNode { @@ -9,6 +12,8 @@ class Background : public settings::ObjectNode { CONFIG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(int, wallFadeDuration, 300) + CONFIG_ENUM_PROPERTY( + WallpaperTransition, wallTransition, WallpaperTransition::Fade) }; } // namespace ZShell::config diff --git a/Plugins/ZShell/Config/enums.hpp b/Plugins/ZShell/Config/enums.hpp index 9ba61a0..f2bb9b5 100644 --- a/Plugins/ZShell/Config/enums.hpp +++ b/Plugins/ZShell/Config/enums.hpp @@ -23,6 +23,19 @@ ENUM(NotifsFullscreen, On, Off) ENUM(TemperatureUnit, Auto, Celsius, Fahrenheit, Kelvin) ENUM(DataUnit, Binary, Decimal) ENUM(ClockFormat, Auto, TwelveHour, TwentyFourHour) +ENUM( + WallpaperTransition, + Bowtie, + Fade, + LinearBlur, + WindowSlices, + WipeRight, + WipeDown, + TilesWave, + SplitSlideInHorizontal, + Hexagonalize, + DoomMelt, + Random) #undef ENUM diff --git a/Plugins/ZShell/Internal/CMakeLists.txt b/Plugins/ZShell/Internal/CMakeLists.txt index a81311e..482460d 100644 --- a/Plugins/ZShell/Internal/CMakeLists.txt +++ b/Plugins/ZShell/Internal/CMakeLists.txt @@ -45,6 +45,7 @@ qml_module(ZShell-internal PkgConfig::AVCODEC PkgConfig::AVFORMAT PkgConfig::AVUTIL + ZShell-config ) qt_add_resources(ZShell-internal "wallpaper_shaders" @@ -53,6 +54,16 @@ qt_add_resources(ZShell-internal "wallpaper_shaders" shaders/wallpaper.vert shaders/wallpaper.frag shaders/wallpaper_video.frag + shaders/wallpaper_trans_bowtie.frag + shaders/wallpaper_trans_fade.frag + shaders/wallpaper_trans_linear_blur.frag + shaders/wallpaper_trans_window_slices.frag + shaders/wallpaper_trans_wipe_right.frag + shaders/wallpaper_trans_wipe_down.frag + shaders/wallpaper_trans_tiles_wave.frag + shaders/wallpaper_trans_split_slide_in_horizontal.frag + shaders/wallpaper_trans_hexagonalize.frag + shaders/wallpaper_trans_doom_melt.frag ) if(CUDAToolkit_FOUND) diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_bowtie.frag b/Plugins/ZShell/Internal/shaders/wallpaper_trans_bowtie.frag new file mode 100644 index 0000000..3806896 --- /dev/null +++ b/Plugins/ZShell/Internal/shaders/wallpaper_trans_bowtie.frag @@ -0,0 +1,106 @@ +// Bow-tie wipe transition adapted from gl-transitions.com +// Author: huynx (MIT) +// Original: two triangles grow from the top and bottom edges into a bow tie +// that reveals the incoming image. + +uniform sampler2D from; +uniform sampler2D to; +uniform vec2 size; +uniform vec4 fromCrop; +uniform vec4 toCrop; +uniform float progress; +uniform float fromFlip; +uniform float toFlip; + +out vec4 outColor; + +vec4 getFromColor(vec2 uv) { + vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(from, fromCrop.xy + s * fromCrop.zw); +} + +vec4 getToColor(vec2 uv) { + vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(to, toCrop.xy + s * toCrop.zw); +} + +float check(vec2 p1, vec2 p2, vec2 p3) { + return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y); +} + +bool pointInTriangle(vec2 pt, vec2 p1, vec2 p2, vec2 p3) { + bool b1 = check(pt, p1, p2) < 0.0; + bool b2 = check(pt, p2, p3) < 0.0; + bool b3 = check(pt, p3, p1) < 0.0; + return (b1 == b2) && (b2 == b3); +} + +bool in_top_triangle(vec2 p) { + vec2 vertex1 = vec2(0.5, progress); + vec2 vertex2 = vec2(0.5 - progress, 0.0); + vec2 vertex3 = vec2(0.5 + progress, 0.0); + return pointInTriangle(p, vertex1, vertex2, vertex3); +} + +bool in_bottom_triangle(vec2 p) { + vec2 vertex1 = vec2(0.5, 1.0 - progress); + vec2 vertex2 = vec2(0.5 - progress, 1.0); + vec2 vertex3 = vec2(0.5 + progress, 1.0); + return pointInTriangle(p, vertex1, vertex2, vertex3); +} + +float blur_edge(vec2 bot1, vec2 bot2, vec2 top, vec2 testPt) { + vec2 lineDir = bot1 - top; + vec2 perpDir = vec2(lineDir.y, -lineDir.x); + vec2 dirToPt1 = bot1 - testPt; + float dist1 = abs(dot(normalize(perpDir), dirToPt1)); + + lineDir = bot2 - top; + perpDir = vec2(lineDir.y, -lineDir.x); + dirToPt1 = bot2 - testPt; + float min_dist = min(abs(dot(normalize(perpDir), dirToPt1)), dist1); + + if (min_dist < 0.005) + return min_dist / 0.005; + return 1.0; +} + +vec4 transition(vec2 uv) { + if (in_top_triangle(uv)) { + if (progress < 0.1) + return getFromColor(uv); + + if (uv.y < 0.5) { + vec2 vertex1 = vec2(0.5, progress); + vec2 vertex2 = vec2(0.5 - progress, 0.0); + vec2 vertex3 = vec2(0.5 + progress, 0.0); + return mix( + getFromColor(uv), + getToColor(uv), + blur_edge(vertex2, vertex3, vertex1, uv)); + } + if (progress > 0.0) + return getToColor(uv); + return getFromColor(uv); + } + + if (in_bottom_triangle(uv)) { + if (uv.y >= 0.5) { + vec2 vertex1 = vec2(0.5, 1.0 - progress); + vec2 vertex2 = vec2(0.5 - progress, 1.0); + vec2 vertex3 = vec2(0.5 + progress, 1.0); + return mix( + getFromColor(uv), + getToColor(uv), + blur_edge(vertex2, vertex3, vertex1, uv)); + } + return getFromColor(uv); + } + + return getFromColor(uv); +} + +void main() { + vec2 uv = gl_FragCoord.xy / size; + outColor = transition(uv); +} diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_doom_melt.frag b/Plugins/ZShell/Internal/shaders/wallpaper_trans_doom_melt.frag new file mode 100644 index 0000000..5b428cd --- /dev/null +++ b/Plugins/ZShell/Internal/shaders/wallpaper_trans_doom_melt.frag @@ -0,0 +1,72 @@ +// Doom-style melt wipe, from gl-transitions.com +// Author: Zeh Fernando (MIT) +// Description: the outgoing image melts down in uneven columns, like the Doom +// screen wipe. Fully procedural (no displacement texture). +// +// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag). Optional +// gl-transitions params are fixed here: bars = 30, amplitude = 2.0, +// noise = 0.1, frequency = 0.5, dripScale = 0.5. + +uniform sampler2D from; +uniform sampler2D to; +uniform vec2 size; +uniform vec4 fromCrop; +uniform vec4 toCrop; +uniform float progress; +uniform float fromFlip; +uniform float toFlip; + +out vec4 outColor; + +const int bars = 30; +const float amplitude = 2.0; +const float noise = 0.1; +const float frequency = 0.5; +const float dripScale = 0.5; + +vec4 getFromColor(vec2 uv) { + vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(from, fromCrop.xy + s * fromCrop.zw); +} + +vec4 getToColor(vec2 uv) { + vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(to, toCrop.xy + s * toCrop.zw); +} + +float rand(int num) { + return fract(mod(float(num) * 67123.313, 12.0) * sin(float(num) * 10.3) * + cos(float(num))); +} + +float wave(int num) { + float fn = float(num) * frequency * 0.1 * float(bars); + return cos(fn * 0.5) * cos(fn * 0.13) * sin((fn + 10.0) * 0.3) / 2.0 + 0.5; +} + +float drip(int num) { + return sin(float(num) / float(bars - 1) * 3.141592) * dripScale; +} + +float pos(int num) { + return (noise == 0.0 ? wave(num) + : mix(wave(num), rand(num), noise)) + + (dripScale == 0.0 ? 0.0 : drip(num)); +} + +vec4 transition(vec2 uv) { + int bar = int(uv.x * float(bars)); + float scale = 1.0 + pos(bar) * amplitude; + float phase = progress * scale; + float posY = uv.y; + + if (phase + posY < 1.0) { + return getFromColor(vec2(uv.x, uv.y + phase)); + } + return getToColor(uv); +} + +void main() { + vec2 uv = gl_FragCoord.xy / size; + outColor = transition(uv); +} diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_fade.frag b/Plugins/ZShell/Internal/shaders/wallpaper_trans_fade.frag new file mode 100644 index 0000000..34a4fce --- /dev/null +++ b/Plugins/ZShell/Internal/shaders/wallpaper_trans_fade.frag @@ -0,0 +1,39 @@ +// Fade (crossfade) transition, from gl-transitions.com. +// +// Ported to ZShell's own GLSL ABI (same contract as wallpaper_trans_bowtie.frag): +// version/precision header is prepended by WallpaperService::shaderSource, the +// fullscreen triangle comes from shaders/wallpaper.vert, and getFromColor / +// getToColor sample with the crop + per-layer flip convention. +// +// uniforms: samplers `from`/`to`, vec2 size, vec4 fromCrop, vec4 toCrop, +// float fromFlip, float toFlip, float progress (0..1). + +uniform sampler2D from; +uniform sampler2D to; +uniform vec2 size; +uniform vec4 fromCrop; +uniform vec4 toCrop; +uniform float fromFlip; +uniform float toFlip; +uniform float progress; + +out vec4 outColor; + +vec4 getFromColor(vec2 uv) { + vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(from, fromCrop.xy + s * fromCrop.zw); +} + +vec4 getToColor(vec2 uv) { + vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(to, toCrop.xy + s * toCrop.zw); +} + +vec4 transition(vec2 uv) { + return mix(getFromColor(uv), getToColor(uv), progress); +} + +void main() { + vec2 uv = gl_FragCoord.xy / size; + outColor = transition(uv); +} diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_hexagonalize.frag b/Plugins/ZShell/Internal/shaders/wallpaper_trans_hexagonalize.frag new file mode 100644 index 0000000..276b777 --- /dev/null +++ b/Plugins/ZShell/Internal/shaders/wallpaper_trans_hexagonalize.frag @@ -0,0 +1,101 @@ +// Hexagonalize crossfade, from gl-transitions.com +// Author: Fernando Kuteken (MIT) +// Description: both images turn into hexagons, crossfade, then sharpen again. +// Hexagonal math from http://www.redblobgames.com/grids/hexagons/ +// +// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag). The +// gl-transitions `ratio` contextual value is derived from `size`. Optional +// params are fixed here: steps = 50, horizontalHexagons = 20. + +uniform sampler2D from; +uniform sampler2D to; +uniform vec2 size; +uniform vec4 fromCrop; +uniform vec4 toCrop; +uniform float progress; +uniform float fromFlip; +uniform float toFlip; + +out vec4 outColor; + +const int steps = 50; +const float horizontalHexagons = 20.0; +float ratio; + +vec4 getFromColor(vec2 uv) { + vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(from, fromCrop.xy + s * fromCrop.zw); +} + +vec4 getToColor(vec2 uv) { + vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(to, toCrop.xy + s * toCrop.zw); +} + +struct Hexagon { + float q; + float r; + float s; +}; + +Hexagon createHexagon(float q, float r) { + Hexagon hex; + hex.q = q; + hex.r = r; + hex.s = -q - r; + return hex; +} + +Hexagon roundHexagon(Hexagon hex) { + float q = floor(hex.q + 0.5); + float r = floor(hex.r + 0.5); + float s = floor(hex.s + 0.5); + + float deltaQ = abs(q - hex.q); + float deltaR = abs(r - hex.r); + float deltaS = abs(s - hex.s); + + if (deltaQ > deltaR && deltaQ > deltaS) + q = -r - s; + else if (deltaR > deltaS) + r = -q - s; + else + s = -q - r; + + return createHexagon(q, r); +} + +Hexagon hexagonFromPoint(vec2 point, float size) { + point.y /= ratio; + point = (point - 0.5) / size; + + float q = (sqrt(3.0) / 3.0) * point.x + (-1.0 / 3.0) * point.y; + float r = 0.0 * point.x + 2.0 / 3.0 * point.y; + + Hexagon hex = createHexagon(q, r); + return roundHexagon(hex); +} + +vec2 pointFromHexagon(Hexagon hex, float size) { + float x = (sqrt(3.0) * hex.q + (sqrt(3.0) / 2.0) * hex.r) * size + 0.5; + float y = (0.0 * hex.q + (3.0 / 2.0) * hex.r) * size + 0.5; + return vec2(x, y * ratio); +} + +vec4 transition(vec2 uv) { + float dist = 2.0 * min(progress, 1.0 - progress); + dist = steps > 0 ? ceil(dist * float(steps)) / float(steps) : dist; + + float size = (sqrt(3.0) / 3.0) * dist / horizontalHexagons; + + vec2 point = dist > 0.0 ? pointFromHexagon(hexagonFromPoint(uv, size), size) + : uv; + + return mix(getFromColor(point), getToColor(point), progress); +} + +void main() { + vec2 uv = gl_FragCoord.xy / size; + ratio = size.x / size.y; + outColor = transition(uv); +} diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_linear_blur.frag b/Plugins/ZShell/Internal/shaders/wallpaper_trans_linear_blur.frag new file mode 100644 index 0000000..00d8a25 --- /dev/null +++ b/Plugins/ZShell/Internal/shaders/wallpaper_trans_linear_blur.frag @@ -0,0 +1,58 @@ +// Linear blur crossfade, from gl-transitions.com +// Author: gre (MIT) +// Description: a crossfade that passes through a directional blur. +// +// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag): the version +// header is prepended by WallpaperService::shaderSource, the fullscreen +// triangle comes from wallpaper.vert, and getFromColor/getToColor apply the +// crop + flip convention. `uv` is bottom-left origin per gl-transitions. +// +// Optional gl-transitions params are fixed here: intensity = 0.1. + +uniform sampler2D from; +uniform sampler2D to; +uniform vec2 size; +uniform vec4 fromCrop; +uniform vec4 toCrop; +uniform float progress; +uniform float fromFlip; +uniform float toFlip; + +out vec4 outColor; + +const float intensity = 0.1; +const int passes = 6; + +vec4 getFromColor(vec2 uv) { + vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(from, fromCrop.xy + s * fromCrop.zw); +} + +vec4 getToColor(vec2 uv) { + vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(to, toCrop.xy + s * toCrop.zw); +} + +vec4 transition(vec2 uv) { + vec4 c1 = vec4(0.0); + vec4 c2 = vec4(0.0); + + float disp = intensity * (0.5 - distance(0.5, progress)); + for (int xi = 0; xi < passes; xi++) { + float x = float(xi) / float(passes) - 0.5; + for (int yi = 0; yi < passes; yi++) { + float y = float(yi) / float(passes) - 0.5; + vec2 v = vec2(x, y); + c1 += getFromColor(uv + disp * v); + c2 += getToColor(uv + disp * v); + } + } + c1 /= float(passes * passes); + c2 /= float(passes * passes); + return mix(c1, c2, progress); +} + +void main() { + vec2 uv = gl_FragCoord.xy / size; + outColor = transition(uv); +} diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_split_slide_in_horizontal.frag b/Plugins/ZShell/Internal/shaders/wallpaper_trans_split_slide_in_horizontal.frag new file mode 100644 index 0000000..9f6b535 --- /dev/null +++ b/Plugins/ZShell/Internal/shaders/wallpaper_trans_split_slide_in_horizontal.frag @@ -0,0 +1,49 @@ +// Split slide-in (horizontal), from gl-transitions.com +// Author: OllyOllyOlly (MIT) +// Description: the incoming image slides in as two halves moving horizontally +// in opposite directions. +// +// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag). +// Optional gl-transitions params are fixed here: reverse = false. + +uniform sampler2D from; +uniform sampler2D to; +uniform vec2 size; +uniform vec4 fromCrop; +uniform vec4 toCrop; +uniform float progress; +uniform float fromFlip; +uniform float toFlip; + +out vec4 outColor; + +const vec2 boundMin = vec2(0.0, 0.0); +const vec2 boundMax = vec2(1.0, 1.0); + +bool inBounds(vec2 p) { + return all(lessThan(boundMin, p)) && all(lessThan(p, boundMax)); +} + +vec4 getFromColor(vec2 uv) { + vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(from, fromCrop.xy + s * fromCrop.zw); +} + +vec4 getToColor(vec2 uv) { + vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(to, toCrop.xy + s * toCrop.zw); +} + +vec4 transition(vec2 uv) { + float modifier = 1.0; + vec2 toP = (uv.y > 0.5) + ? vec2((uv.x - (progress * modifier)) + modifier, uv.y) + : vec2((uv.x + (progress * modifier)) - modifier, uv.y); + + return inBounds(toP) ? getToColor(toP) : getFromColor(uv); +} + +void main() { + vec2 uv = gl_FragCoord.xy / size; + outColor = transition(uv); +} diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_tiles_wave.frag b/Plugins/ZShell/Internal/shaders/wallpaper_trans_tiles_wave.frag new file mode 100644 index 0000000..5aacf50 --- /dev/null +++ b/Plugins/ZShell/Internal/shaders/wallpaper_trans_tiles_wave.frag @@ -0,0 +1,89 @@ +// Tiles wave reveal, from gl-transitions.com +// Author: numb3r23 (MIT) +// Description: the incoming image is revealed tile by tile in a diagonal wave. +// +// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag). +// Optional gl-transitions params are fixed here: tileCount = (8, 8), +// flipX = true, flipY = false. + +uniform sampler2D from; +uniform sampler2D to; +uniform vec2 size; +uniform vec4 fromCrop; +uniform vec4 toCrop; +uniform float progress; +uniform float fromFlip; +uniform float toFlip; + +out vec4 outColor; + +const ivec2 tileCount = ivec2(8, 8); +const bool flipX = true; +const bool flipY = false; + +vec4 getFromColor(vec2 uv) { + vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(from, fromCrop.xy + s * fromCrop.zw); +} + +vec4 getToColor(vec2 uv) { + vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(to, toCrop.xy + s * toCrop.zw); +} + +vec4 transition(vec2 uv) { + vec2 tileSize = 1.0 / vec2(tileCount); + vec2 posInTile = fract(uv * vec2(tileCount)); + vec2 tileNum = floor(uv * vec2(tileCount)); + float countTiles = float(tileCount.x * tileCount.y); + + // Diagonal wave from bottom-left to top-right. + float offset = (tileNum.y + tileNum.x * float(tileCount.y)) / countTiles; + float timeOffset = clamp((progress - offset) * countTiles, 0.0, 0.5); + float sinTime = 1.0 - abs(cos(fract(timeOffset) * 3.1415926)); + + vec2 texC = posInTile; + + if (sinTime <= 0.5) { + if (flipX) { + if (texC.x < sinTime || texC.x > 1.0 - sinTime) + return getFromColor(uv); + texC.x = texC.x < 0.5 + ? (texC.x - sinTime) * 0.5 / (0.5 - sinTime) + : (texC.x - 0.5) * 0.5 / (0.5 - sinTime) + 0.5; + } + if (flipY) { + if (texC.y < sinTime || texC.y > 1.0 - sinTime) + return getFromColor(uv); + texC.y = texC.y < 0.5 + ? (texC.y - sinTime) * 0.5 / (0.5 - sinTime) + : (texC.y - 0.5) * 0.5 / (0.5 - sinTime) + 0.5; + } + vec2 globalUV = tileNum * tileSize + texC * tileSize; + return getFromColor(globalUV); + } else { + if (flipX) { + if (texC.x > sinTime || texC.x < 1.0 - sinTime) + return getToColor(uv); + texC.x = texC.x < 0.5 + ? (texC.x - sinTime) * 0.5 / (0.5 - sinTime) + : (texC.x - 0.5) * 0.5 / (0.5 - sinTime) + 0.5; + texC.x = 1.0 - texC.x; + } + if (flipY) { + if (texC.y > sinTime || texC.y < 1.0 - sinTime) + return getToColor(uv); + texC.y = texC.y < 0.5 + ? (texC.y - sinTime) * 0.5 / (0.5 - sinTime) + : (texC.y - 0.5) * 0.5 / (0.5 - sinTime) + 0.5; + texC.y = 1.0 - texC.y; + } + vec2 globalUV = tileNum * tileSize + texC * tileSize; + return getToColor(globalUV); + } +} + +void main() { + vec2 uv = gl_FragCoord.xy / size; + outColor = transition(uv); +} diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_window_slices.frag b/Plugins/ZShell/Internal/shaders/wallpaper_trans_window_slices.frag new file mode 100644 index 0000000..44d56c0 --- /dev/null +++ b/Plugins/ZShell/Internal/shaders/wallpaper_trans_window_slices.frag @@ -0,0 +1,42 @@ +// Window slices wipe, from gl-transitions.com +// Author: gre (MIT) +// Description: the incoming image appears through vertical slices that widen. +// +// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag). +// Optional gl-transitions params are fixed here: count = 10.0, +// smoothness = 0.5. + +uniform sampler2D from; +uniform sampler2D to; +uniform vec2 size; +uniform vec4 fromCrop; +uniform vec4 toCrop; +uniform float progress; +uniform float fromFlip; +uniform float toFlip; + +out vec4 outColor; + +const float count = 10.0; +const float smoothness = 0.5; + +vec4 getFromColor(vec2 uv) { + vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(from, fromCrop.xy + s * fromCrop.zw); +} + +vec4 getToColor(vec2 uv) { + vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(to, toCrop.xy + s * toCrop.zw); +} + +vec4 transition(vec2 uv) { + float pr = smoothstep(-smoothness, 0.0, uv.x - progress * (1.0 + smoothness)); + float s = step(pr, fract(count * uv.x)); + return mix(getFromColor(uv), getToColor(uv), s); +} + +void main() { + vec2 uv = gl_FragCoord.xy / size; + outColor = transition(uv); +} diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_wipe_down.frag b/Plugins/ZShell/Internal/shaders/wallpaper_trans_wipe_down.frag new file mode 100644 index 0000000..fbab8d6 --- /dev/null +++ b/Plugins/ZShell/Internal/shaders/wallpaper_trans_wipe_down.frag @@ -0,0 +1,34 @@ +// Hard-edged wipe from top to bottom, from gl-transitions.com +// Author: Jake Nelson (MIT) +// +// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag). + +uniform sampler2D from; +uniform sampler2D to; +uniform vec2 size; +uniform vec4 fromCrop; +uniform vec4 toCrop; +uniform float progress; +uniform float fromFlip; +uniform float toFlip; + +out vec4 outColor; + +vec4 getFromColor(vec2 uv) { + vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(from, fromCrop.xy + s * fromCrop.zw); +} + +vec4 getToColor(vec2 uv) { + vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(to, toCrop.xy + s * toCrop.zw); +} + +vec4 transition(vec2 uv) { + return mix(getFromColor(uv), getToColor(uv), step(1.0 - uv.y, progress)); +} + +void main() { + vec2 uv = gl_FragCoord.xy / size; + outColor = transition(uv); +} diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_wipe_right.frag b/Plugins/ZShell/Internal/shaders/wallpaper_trans_wipe_right.frag new file mode 100644 index 0000000..51391b5 --- /dev/null +++ b/Plugins/ZShell/Internal/shaders/wallpaper_trans_wipe_right.frag @@ -0,0 +1,34 @@ +// Hard-edged wipe from left to right, from gl-transitions.com +// Author: Jake Nelson (MIT) +// +// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag). + +uniform sampler2D from; +uniform sampler2D to; +uniform vec2 size; +uniform vec4 fromCrop; +uniform vec4 toCrop; +uniform float progress; +uniform float fromFlip; +uniform float toFlip; + +out vec4 outColor; + +vec4 getFromColor(vec2 uv) { + vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(from, fromCrop.xy + s * fromCrop.zw); +} + +vec4 getToColor(vec2 uv) { + vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y); + return texture(to, toCrop.xy + s * toCrop.zw); +} + +vec4 transition(vec2 uv) { + return mix(getFromColor(uv), getToColor(uv), step(0.0 + uv.x, progress)); +} + +void main() { + vec2 uv = gl_FragCoord.xy / size; + outColor = transition(uv); +} diff --git a/Plugins/ZShell/Internal/videoplayer.cpp b/Plugins/ZShell/Internal/videoplayer.cpp index ba49577..ed14b78 100644 --- a/Plugins/ZShell/Internal/videoplayer.cpp +++ b/Plugins/ZShell/Internal/videoplayer.cpp @@ -184,9 +184,13 @@ void VideoPlayer::closeDecoder() { m_streamIndex = -1; } -void VideoPlayer::decodeLoop(const QString& path) { +void VideoPlayer::decodeLoop(const QString& path, uint32_t generation) { + if (!m_threadRunning.load(std::memory_order_acquire) || + m_generation.load(std::memory_order_acquire) != generation) + return; + if (!openDecoder(path)) { - m_threadRunning = false; + m_threadRunning.store(false); return; } @@ -198,13 +202,14 @@ void VideoPlayer::decodeLoop(const QString& path) { AVFrame* frame = av_frame_alloc(); if (!frame) { - m_threadRunning = false; + m_threadRunning.store(false); return; } AVPacket pkt{}; - while (m_threadRunning) { + while (m_threadRunning.load(std::memory_order_acquire) && + m_generation.load(std::memory_order_acquire) == generation) { int ret = av_read_frame(m_fmt, &pkt); if (ret == AVERROR_EOF) { if (avformat_seek_file( @@ -230,7 +235,9 @@ void VideoPlayer::decodeLoop(const QString& path) { } av_packet_unref(&pkt); - while (m_threadRunning && avcodec_receive_frame(m_codec, frame) == 0) { + while (m_threadRunning.load(std::memory_order_acquire) && + m_generation.load(std::memory_order_acquire) == generation && + avcodec_receive_frame(m_codec, frame) == 0) { bool wasEmpty = false; { std::lock_guard lock(m_mtx); @@ -251,21 +258,34 @@ void VideoPlayer::decodeLoop(const QString& path) { av_frame_free(&frame); closeDecoder(); - m_threadRunning = false; + m_threadRunning.store(false); } bool VideoPlayer::start(const QString& path) { - stop(); + const uint32_t generation = + m_generation.fetch_add(1, std::memory_order_acq_rel) + 1; + m_threadRunning.store(true); - if (!ensureCudaContext()) return false; + std::thread previous = std::move(m_thread); + m_thread = std::thread( + [this, path, generation, previous = std::move(previous)]() mutable { + if (previous.joinable()) previous.join(); + closeDecoder(); + + if (!ensureCudaContext()) { + m_threadRunning.store(false); + return; + } + + decodeLoop(path, generation); + }); - m_threadRunning = true; - m_thread = std::thread([this, path]() { decodeLoop(path); }); return true; } void VideoPlayer::stop() { - m_threadRunning = false; + m_generation.fetch_add(1, std::memory_order_acq_rel); + m_threadRunning.store(false); if (m_thread.joinable()) m_thread.join(); closeDecoder(); diff --git a/Plugins/ZShell/Internal/videoplayer.hpp b/Plugins/ZShell/Internal/videoplayer.hpp index b6137e0..ed7d6e4 100644 --- a/Plugins/ZShell/Internal/videoplayer.hpp +++ b/Plugins/ZShell/Internal/videoplayer.hpp @@ -46,7 +46,7 @@ class VideoPlayer { [[nodiscard]] QSize frameSize() const; private: - void decodeLoop(const QString& path); + void decodeLoop(const QString& path, uint32_t generation); bool openDecoder(const QString& path); void closeDecoder(); @@ -60,6 +60,7 @@ class VideoPlayer { std::thread m_thread; std::atomic m_threadRunning{false}; + std::atomic m_generation{0}; mutable std::mutex m_mtx; AVFrame* m_pending = nullptr; diff --git a/Plugins/ZShell/Internal/wallpaperobject.cpp b/Plugins/ZShell/Internal/wallpaperobject.cpp index b11057e..b29ce9c 100644 --- a/Plugins/ZShell/Internal/wallpaperobject.cpp +++ b/Plugins/ZShell/Internal/wallpaperobject.cpp @@ -67,8 +67,46 @@ void WallpaperObject::setZoom(qreal zoom) { push(); } +void WallpaperObject::syncCrop(const QVariantMap& crop) { + const qreal x = crop.value("x").toDouble(); + const qreal y = crop.value("y").toDouble(); + const qreal width = crop.contains("width") ? crop.value("width").toDouble() + : 1.0; + const qreal height = crop.contains("height") + ? crop.value("height").toDouble() + : 1.0; + const qreal z = + crop.contains("zoom") ? crop.value("zoom").toDouble() : 1.0; + + const bool prev = m_pushEnabled; + m_pushEnabled = false; + + if (!qFuzzyCompare(m_cropX, x)) { + m_cropX = x; + emit cropXChanged(); + } + if (!qFuzzyCompare(m_cropY, y)) { + m_cropY = y; + emit cropYChanged(); + } + if (!qFuzzyCompare(m_cropWidth, width)) { + m_cropWidth = width; + emit cropWidthChanged(); + } + if (!qFuzzyCompare(m_cropHeight, height)) { + m_cropHeight = height; + emit cropHeightChanged(); + } + if (!qFuzzyCompare(m_zoom, z)) { + m_zoom = z; + emit zoomChanged(); + } + + m_pushEnabled = prev; +} + void WallpaperObject::push() { - if (!m_service || m_screen.isEmpty()) return; + if (!m_pushEnabled || !m_service || m_screen.isEmpty()) return; QVariantMap crop; crop.insert("x", m_cropX); diff --git a/Plugins/ZShell/Internal/wallpaperobject.hpp b/Plugins/ZShell/Internal/wallpaperobject.hpp index eb3d8a4..2787ded 100644 --- a/Plugins/ZShell/Internal/wallpaperobject.hpp +++ b/Plugins/ZShell/Internal/wallpaperobject.hpp @@ -55,6 +55,8 @@ class WallpaperObject : public QObject { qreal zoom() const { return m_zoom; } void setZoom(qreal zoom); + Q_INVOKABLE void syncCrop(const QVariantMap& crop); + signals: void serviceChanged(); void screenChanged(); @@ -77,6 +79,8 @@ class WallpaperObject : public QObject { qreal m_cropWidth = 1.0; qreal m_cropHeight = 1.0; qreal m_zoom = 1.0; + + bool m_pushEnabled = true; }; } // namespace ZShell::internal diff --git a/Plugins/ZShell/Internal/wallpaperservice.cpp b/Plugins/ZShell/Internal/wallpaperservice.cpp index 9b512e8..b8569a0 100644 --- a/Plugins/ZShell/Internal/wallpaperservice.cpp +++ b/Plugins/ZShell/Internal/wallpaperservice.cpp @@ -25,8 +25,12 @@ #include #include #include +#include +#include +#include #include #include +#include #include #include #include @@ -40,6 +44,9 @@ #include +#include "config/background.hpp" +#include "config/rootnodes.hpp" + #include namespace ZShell::internal { @@ -77,6 +84,51 @@ QByteArray shaderSource(const QString& file, bool fragment) { return QByteArray(shaderHeader(fragment)) + source.readAll(); } +const char* transitionFile(ZShell::config::WallpaperTransition::Enum value) { + switch (value) { + case ZShell::config::WallpaperTransition::Bowtie: + return "wallpaper_trans_bowtie.frag"; + case ZShell::config::WallpaperTransition::Fade: + return "wallpaper_trans_fade.frag"; + case ZShell::config::WallpaperTransition::LinearBlur: + return "wallpaper_trans_linear_blur.frag"; + case ZShell::config::WallpaperTransition::WindowSlices: + return "wallpaper_trans_window_slices.frag"; + case ZShell::config::WallpaperTransition::WipeRight: + return "wallpaper_trans_wipe_right.frag"; + case ZShell::config::WallpaperTransition::WipeDown: + return "wallpaper_trans_wipe_down.frag"; + case ZShell::config::WallpaperTransition::TilesWave: + return "wallpaper_trans_tiles_wave.frag"; + case ZShell::config::WallpaperTransition::SplitSlideInHorizontal: + return "wallpaper_trans_split_slide_in_horizontal.frag"; + case ZShell::config::WallpaperTransition::Hexagonalize: + return "wallpaper_trans_hexagonalize.frag"; + case ZShell::config::WallpaperTransition::DoomMelt: + return "wallpaper_trans_doom_melt.frag"; + case ZShell::config::WallpaperTransition::Random: + break; + } + + return "wallpaper_trans_fade.frag"; +} + +const QStringList& transitionFilesList() { + static const QStringList files = { + "wallpaper_trans_bowtie.frag", + "wallpaper_trans_fade.frag", + "wallpaper_trans_linear_blur.frag", + "wallpaper_trans_window_slices.frag", + "wallpaper_trans_wipe_right.frag", + "wallpaper_trans_wipe_down.frag", + "wallpaper_trans_tiles_wave.frag", + "wallpaper_trans_split_slide_in_horizontal.frag", + "wallpaper_trans_hexagonalize.frag", + "wallpaper_trans_doom_melt.frag", + }; + return files; +} + constexpr int kWaitTimeoutMs = 20; constexpr int kFadeDurationMs = 400; @@ -133,7 +185,35 @@ QVariantMap cropFor(const QVariantMap& crops, const QString& screen) { } // namespace -WallpaperService::WallpaperService(QObject* parent) : QObject(parent) {} +WallpaperService::WallpaperService(QObject* parent) : QObject(parent) { + auto* background = + ZShell::config::ConfigSingleton::instance()->background(); + + { + std::lock_guard lock(m_mutex); + m_randomTransition = background->wallTransition() == + ZShell::config::WallpaperTransition::Random; + m_transitionFile = transitionFileFor(background->wallTransition()); + } + + QObject::connect( + background, + &ZShell::config::Background::wallTransitionChanged, + this, + [this, background] { + { + std::lock_guard lock(m_mutex); + m_randomTransition = + background->wallTransition() == + ZShell::config::WallpaperTransition::Random; + m_transitionFile = + transitionFileFor(background->wallTransition()); + m_transitionDirty = true; + } + + wake(); + }); +} WallpaperService::~WallpaperService() { m_stop = true; @@ -159,13 +239,27 @@ WallpaperService::~WallpaperService() { if (eglMakeCurrent( m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, m_context)) { if (m_texture) glDeleteTextures(1, &m_texture); - if (m_previousTexture) glDeleteTextures(1, &m_previousTexture); + for (const auto& layer : m_transitions) + if (layer.tex) glDeleteTextures(1, &layer.tex); + m_transitions.clear(); + if (m_baseTex) glDeleteTextures(1, &m_baseTex); + if (m_baseFbo) glDeleteFramebuffers(1, &m_baseFbo); + if (m_accumA) glDeleteTextures(1, &m_accumA); + if (m_accumB) glDeleteTextures(1, &m_accumB); + if (m_accumFbo) glDeleteFramebuffers(1, &m_accumFbo); for (uint32_t tex : m_orphanTextures) glDeleteTextures(1, &tex); m_orphanTextures.clear(); if (m_blackTex) glDeleteTextures(1, &m_blackTex); if (m_program) glDeleteProgram(m_program); if (m_videoProgram) glDeleteProgram(m_videoProgram); + for (const auto& p : m_transPrograms) + if (p.id) glDeleteProgram(p.id); + m_transPrograms.clear(); + m_neutralProgram = TransProgram(); + if (m_captureTex) glDeleteTextures(1, &m_captureTex); + if (m_captureFbo) glDeleteFramebuffers(1, &m_captureFbo); + if (m_posterTex) glDeleteTextures(1, &m_posterTex); #ifdef ZHAVE_VIDEO if (m_player) { m_player->stop(); @@ -237,8 +331,17 @@ void WallpaperService::applyPath(const QString& path) { { std::lock_guard lock(m_mutex); + const QString previousPath = m_path; m_path = path; + if (m_randomTransition) { + const QString next = randomTransitionFile(); + if (next != m_transitionFile) { + m_transitionFile = next; + m_transitionDirty = true; + } + } + #ifdef ZHAVE_VIDEO const bool videoBackend = m_mpv != nullptr || m_player != nullptr; #else @@ -246,28 +349,72 @@ void WallpaperService::applyPath(const QString& path) { #endif const bool wasVideo = m_isVideo && videoBackend; const bool wasImage = !m_image.isNull() && m_texture != 0; + const bool beginBurst = m_transitions.empty(); - m_previousVideo = wasVideo && !video; - - if (wasImage) { - if (m_previousTexture != 0) - m_orphanTextures.push_back(m_previousTexture); - m_previousImage = m_image; - m_previousCrops = m_crops; - m_previousTexture = 0; - } else { - m_previousImage = QImage(); + if (beginBurst) { + if (wasVideo) { + m_needCaptureBase = true; + m_baseIsVideo = true; + m_baseIsBlack = false; + m_baseImage = QImage(); + m_baseCrops = cropsForPath(previousPath); + } else if (wasImage) { + m_needCaptureBase = true; + m_baseIsVideo = false; + m_baseIsBlack = false; + m_baseImage = m_image; + m_baseCrops = cropsForPath(previousPath); + } else { + m_needCaptureBase = false; + m_baseIsVideo = false; + m_baseIsBlack = m_baseTex == 0; + m_baseImage = QImage(); + m_baseCrops = cropsForPath(previousPath); + } } + if (!beginBurst && !m_transitions.empty() && + m_transitions.back().isVideo && !m_transitions.back().started && + !m_posterReady) + m_transitions.pop_back(); + + if (!beginBurst && !m_transitions.empty() && m_texture != 0 && + !m_transitions.back().isVideo) { + TransitionLayer& newest = m_transitions.back(); + newest.tex = m_texture; + newest.image = m_image; + newest.crops = cropsForPath(previousPath); + newest.ready = true; + m_texture = 0; + m_image = QImage(); + } + + if (!beginBurst && video && !m_transitions.empty() && + m_transitions.back().isVideo) + m_collapseVideo = true; + + m_crops = cropsForPath(path); + m_isVideo = video; - m_fading = true; - m_fadeClockStarted = false; m_replacementReady = false; - m_forceDecode = true; m_imageChanged = true; m_videoReload = true; + + TransitionLayer layer; + layer.isVideo = video; + layer.transitionFile = m_transitionFile; + m_transitions.push_back(layer); + + if (static_cast(m_transitions.size()) > kMaxTransitions) + m_promoteOldest = true; + + while (static_cast(m_transitions.size()) > kMaxTransitions * 2) { + if (m_transitions.front().tex != 0) + m_orphanTextures.push_back(m_transitions.front().tex); + m_transitions.erase(m_transitions.begin()); + } } emit pathChanged(); @@ -795,13 +942,14 @@ void WallpaperService::present() { ensureImage(); + ensurePoster(); + if (m_isVideo && m_videoReady.load(std::memory_order_relaxed)) { std::lock_guard lock(m_mutex); m_replacementReady = true; } QList surfaces; - bool fading = false; bool videoReload = false; QString path; bool isVideo = false; @@ -813,12 +961,12 @@ void WallpaperService::present() { path = m_path; isVideo = m_isVideo; - fading = m_fading; + const bool animating = !m_transitions.empty(); const bool newVideoFrame = m_isVideo && m_mpvFrameReady.exchange(false, std::memory_order_relaxed); - if (fading || newVideoFrame) + if (animating || newVideoFrame) for (auto* surface : m_surfaces) surface->markDirty(); @@ -855,24 +1003,37 @@ void WallpaperService::present() { m_orphanTextures.clear(); } - if (videoReload) { - if (isVideo) + const bool holdVideoForBase = (m_needCaptureBase && m_baseIsVideo) || + m_collapseVideo; + + auto applyVideoBackendSwitch = [&]() { + if (!videoReload) return; + + std::lock_guard lock(m_mutex); + + if (isVideo) { startVideo(path); - else if (m_mpv && !m_previousVideo) + } else if (m_mpv != nullptr) { teardownMpv(); + } #ifdef ZHAVE_VIDEO - else if (m_player && !m_previousVideo) { + else if (m_player != nullptr) { m_player->deleteTexture(); stopPlayerVideo(); } #endif - } + }; + + if (!holdVideoForBase) applyVideoBackendSwitch(); #ifdef ZHAVE_VIDEO - const bool videoActive = (m_isVideo || m_previousVideo) && - (m_mpv != nullptr || m_player != nullptr); + const bool videoActive = + (m_mpv != nullptr || m_player != nullptr) && + (m_isVideo || m_baseIsVideo || m_collapseVideo || m_promoteOldest); #else - const bool videoActive = (m_isVideo || m_previousVideo) && m_mpv != nullptr; + const bool videoActive = + m_mpv != nullptr && + (m_isVideo || m_baseIsVideo || m_collapseVideo || m_promoteOldest); #endif if (videoActive && m_mpv != nullptr) pumpMpvEvents(); @@ -883,6 +1044,45 @@ void WallpaperService::present() { for (auto* surface : surfaces) drawSurface(surface); + + if (holdVideoForBase) applyVideoBackendSwitch(); + + { + std::lock_guard lock(m_mutex); + + if (m_collapseVideo) { + for (size_t i = 0; i + 1 < m_transitions.size(); ++i) + if (m_transitions[i].tex != 0) + m_orphanTextures.push_back(m_transitions[i].tex); + if (m_transitions.size() > 1) + m_transitions.erase( + m_transitions.begin(), m_transitions.end() - 1); + m_collapseVideo = false; + } + + if (m_promoteOldest && !m_transitions.empty()) { + if (m_transitions.front().tex != 0) + m_orphanTextures.push_back(m_transitions.front().tex); + m_transitions.erase(m_transitions.begin()); + m_promoteOldest = static_cast(m_transitions.size()) > + kMaxTransitions; + } + + if (m_newestDone) { + for (const auto& layer : m_transitions) + if (layer.tex != 0) m_orphanTextures.push_back(layer.tex); + m_transitions.clear(); + m_newestDone = false; + m_baseIsBlack = false; + m_baseImage = QImage(); + m_baseCrops = QVariantMap(); + } + + if (m_needCaptureBase) { + m_needCaptureBase = false; + m_baseIsVideo = false; + } + } } void WallpaperService::addSurfaces() { @@ -1066,6 +1266,164 @@ uint32_t WallpaperService::createProgram() { return program; } +WallpaperService::TransProgram WallpaperService::createTransitionProgram( + const QString& file) { + TransProgram p; + + const QByteArray vsSrc = shaderSource("wallpaper.vert", false); + const QByteArray fsSrc = shaderSource(file, true); + const char* vs = vsSrc.constData(); + const char* fs = fsSrc.constData(); + + uint32_t vertex = glCreateShader(GL_VERTEX_SHADER); + glShaderSource(vertex, 1, &vs, nullptr); + glCompileShader(vertex); + logShader(vertex, false); + + uint32_t fragment = glCreateShader(GL_FRAGMENT_SHADER); + glShaderSource(fragment, 1, &fs, nullptr); + glCompileShader(fragment); + logShader(fragment, false); + + uint32_t program = glCreateProgram(); + glAttachShader(program, vertex); + glAttachShader(program, fragment); + glLinkProgram(program); + logShader(program, true); + + if (glGetUniformLocation(program, "size") < 0 || + glGetUniformLocation(program, "from") < 0 || + glGetUniformLocation(program, "to") < 0 || + glGetUniformLocation(program, "fromCrop") < 0 || + glGetUniformLocation(program, "toCrop") < 0 || + glGetUniformLocation(program, "fromFlip") < 0 || + glGetUniformLocation(program, "toFlip") < 0 || + glGetUniformLocation(program, "progress") < 0) { + qWarning(wpLog) << "WallpaperService: the transition program does not " + "have the uniforms it needs"; + glDeleteProgram(program); + glDeleteShader(vertex); + glDeleteShader(fragment); + return p; + } + + p.id = program; + p.size = glGetUniformLocation(program, "size"); + p.progress = glGetUniformLocation(program, "progress"); + p.from = glGetUniformLocation(program, "from"); + p.to = glGetUniformLocation(program, "to"); + p.fromCrop = glGetUniformLocation(program, "fromCrop"); + p.toCrop = glGetUniformLocation(program, "toCrop"); + p.fromFlip = glGetUniformLocation(program, "fromFlip"); + p.toFlip = glGetUniformLocation(program, "toFlip"); + + glUseProgram(program); + glUniform1i(GLint(p.from), 0); + glUniform1i(GLint(p.to), 1); + + glDeleteShader(vertex); + glDeleteShader(fragment); + return p; +} + +WallpaperService::TransProgram WallpaperService::programFor( + const QString& file) { + auto cached = m_transPrograms.constFind(file); + if (cached != m_transPrograms.constEnd()) return cached.value(); + + TransProgram p = createTransitionProgram(file); + if (p.id != 0) m_transPrograms.insert(file, p); + return p; +} + +bool WallpaperService::ensureTransitionProgram() { + if (m_neutralProgram.id == 0) + m_neutralProgram = + programFor(QStringLiteral("wallpaper_trans_fade.frag")); + + return m_neutralProgram.id != 0; +} + +QString WallpaperService::transitionFileFor( + ZShell::config::WallpaperTransition::Enum value) { + if (value == ZShell::config::WallpaperTransition::Random) + return randomTransitionFile(); + return QString(transitionFile(value)); +} + +QString WallpaperService::randomTransitionFile() { + const QStringList& files = transitionFilesList(); + if (files.isEmpty()) return QStringLiteral("wallpaper_trans_fade.frag"); + if (files.size() == 1) return files.at(0); + + if (m_transitionUses.size() != files.size()) + m_transitionUses.fill(0, files.size()); + + double total = 0.0; + QVector weights(files.size(), 0.0); + for (int i = 0; i < files.size(); ++i) { + if (files.at(i) == m_lastTransition) continue; + weights[i] = 1.0 / double(m_transitionUses.at(i) + 1); + total += weights[i]; + } + + if (total <= 0.0) return files.at(0); + + double pick = QRandomGenerator::global()->generateDouble() * total; + int chosen = -1; + for (int i = 0; i < files.size(); ++i) { + if (weights[i] <= 0.0) continue; + pick -= weights[i]; + if (pick <= 0.0) { + chosen = i; + break; + } + } + if (chosen < 0) chosen = int(files.size()) - 1; + + ++m_transitionUses[chosen]; + m_lastTransition = files.at(chosen); + return files.at(chosen); +} + +void WallpaperService::ensureCapture(const QSize& size) { + if (m_captureFbo == 0) { + glGenFramebuffers(1, &m_captureFbo); + glGenTextures(1, &m_captureTex); + + glBindTexture(GL_TEXTURE_2D, m_captureTex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + + if (m_captureSize != size) { + glBindTexture(GL_TEXTURE_2D, m_captureTex); + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RGBA8, + size.width(), + size.height(), + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + nullptr); + m_captureSize = size; + } + + glBindFramebuffer(GL_FRAMEBUFFER, m_captureFbo); + glFramebufferTexture2D( + GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_captureTex, 0); + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + qWarning(wpLog) << "WallpaperService: the capture framebuffer is " + "incomplete"; + + glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + #ifdef ZHAVE_VIDEO uint32_t WallpaperService::createVideoProgram() { const QByteArray vsSrc = shaderSource("wallpaper.vert", false); @@ -1184,9 +1542,73 @@ void WallpaperService::stopPlayerVideo() { m_player->stop(); m_player.reset(); + + m_videoReady.store(false, std::memory_order_relaxed); + m_videoLoaded.store(false, std::memory_order_relaxed); } #endif +uint32_t WallpaperService::renderVideoIntoTexture(const QSize& size) { + ensureCapture(size); + if (m_captureFbo == 0 || m_captureTex == 0) return 0; + + glBindFramebuffer(GL_FRAMEBUFFER, m_captureFbo); + glViewport(0, 0, size.width(), size.height()); + glDisable(GL_BLEND); + +#ifdef ZHAVE_VIDEO + if (m_player) { + if (m_player->textureId() != 0) { + if (m_videoProgram == 0) m_videoProgram = createVideoProgram(); + + if (m_videoProgram != 0) { + glUseProgram(m_videoProgram); + const QSize src = m_player->frameSize(); + drawVideoTexture( + m_player->textureId(), + m_player->chromaTextureId(), + m_player->colorMatrix(), + m_player->colorRange(), + QRectF(0.0, 0.0, 1.0, 1.0), + float(size.width()), + float(size.height()), + src.width() > 0 ? float(src.width()) : float(size.width()), + src.height() > 0 ? float(src.height()) + : float(size.height()), + 1.0f); + } + } + } else +#endif + { + if (!m_mpvRender) initMpvRender(); + if (!m_mpvRender) { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + return m_captureTex; + } + + glViewport(0, 0, size.width(), size.height()); + + mpv_opengl_fbo fbo{ + int(m_captureFbo), size.width(), size.height(), GL_RGBA}; + + int flip = 1; + int block = 0; + mpv_render_param params[]{ + {MPV_RENDER_PARAM_OPENGL_FBO, &fbo}, + {MPV_RENDER_PARAM_FLIP_Y, &flip}, + {MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME, &block}, + {MPV_RENDER_PARAM_INVALID, nullptr}, + }; + + mpv_render_context_render(m_mpvRender, params); + } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + return m_captureTex; +} + + void WallpaperService::uploadTexture(uint32_t& texture, const QImage& image) { if (image.isNull()) return; @@ -1282,6 +1704,108 @@ QByteArray WallpaperService::cachedBlob(const QString& path) const { return blob; } +QByteArray WallpaperService::posterBlob(const QString& path) const { + if (path.isEmpty()) return {}; + + QString databasePath; + { + std::lock_guard lock(m_mutex); + databasePath = m_dbPath; + } + + if (databasePath.isEmpty()) { + qWarning(wpLog) << "WallpaperService: poster lookup, no database path"; + return {}; + } + + const QFileInfo info(path); + + const QString connection = QStringLiteral("wallpaper-cache-%1") + .arg(quintptr(QThread::currentThreadId())); + + QSqlDatabase database = + QSqlDatabase::contains(connection) + ? QSqlDatabase::database(connection, false) + : QSqlDatabase::addDatabase("QSQLITE", connection); + + if (database.databaseName() != databasePath) { + if (database.isOpen()) database.close(); + database.setDatabaseName(databasePath); + } + + if (!database.isOpen() && !database.open()) { + qWarning(wpLog) + << "WallpaperService: poster lookup, database could not open"; + return {}; + } + + QSqlQuery query(database); + query.prepare("SELECT poster, size, mtime FROM wallpapers WHERE path = ?"); + query.addBindValue(info.absoluteFilePath()); + if (!query.exec() || !query.next()) { + qWarning(wpLog) << "WallpaperService: poster lookup, no row for" << path + << query.lastError().text(); + return {}; + } + + const QByteArray blob = query.value(0).toByteArray(); + if (blob.isEmpty()) { + qWarning(wpLog) << "WallpaperService: poster column empty for" << path; + return {}; + } + + const qint64 mtime = info.lastModified().toMSecsSinceEpoch() / 1000; + if (query.value(1).toLongLong() != info.size() || + query.value(2).toLongLong() != mtime) { + qWarning(wpLog) << "WallpaperService: poster stale for" << path; + return {}; + } + + return blob; +} + +QVariantMap WallpaperService::cropsForPath(const QString& path) const { + if (path.isEmpty()) return {}; + + QString databasePath; + { + std::lock_guard lock(m_mutex); + databasePath = m_dbPath; + } + + if (databasePath.isEmpty()) return {}; + + const QFileInfo info(path); + + const QString connection = QStringLiteral("wallpaper-cache-%1") + .arg(quintptr(QThread::currentThreadId())); + + QSqlDatabase database = + QSqlDatabase::contains(connection) + ? QSqlDatabase::database(connection, false) + : QSqlDatabase::addDatabase("QSQLITE", connection); + + if (database.databaseName() != databasePath) { + if (database.isOpen()) database.close(); + database.setDatabaseName(databasePath); + } + + if (!database.isOpen() && !database.open()) return {}; + + QSqlQuery query(database); + query.prepare("SELECT crop FROM wallpapers WHERE path = ?"); + query.addBindValue(info.absoluteFilePath()); + if (!query.exec() || !query.next()) return {}; + + const QByteArray json = query.value(0).toByteArray(); + if (json.isEmpty()) return {}; + + const auto doc = QJsonDocument::fromJson(json); + if (!doc.isObject()) return {}; + + return doc.object().toVariantMap(); +} + QImage WallpaperService::decodeImage( const QString& path, const QSize& decodeSize) const { if (path.isEmpty()) return {}; @@ -1379,6 +1903,48 @@ void WallpaperService::ensureImage() { wake(); } +void WallpaperService::ensurePoster() { + QString path; + + { + std::lock_guard lock(m_mutex); + + if (!m_isVideo) return; + + path = m_path; + + if (m_posterPath == path && (m_posterReady || m_posterLoading)) return; + if (m_posterLoading) return; + + m_posterPath = path; + m_posterReady = false; + m_posterLoading = true; + m_posterImage = QImage(); + } + + QThreadPool::globalInstance()->start([this, path]() { + const QByteArray blob = posterBlob(path); + + QImage image; + if (!blob.isEmpty()) { + image = QImage::fromData(blob, "JPEG") + .convertToFormat(QImage::Format_RGBA8888); + } + + std::lock_guard lock(m_mutex); + m_posterImage = image; + m_posterReady = !image.isNull(); + m_posterChanged = m_posterReady; + m_posterLoading = false; + + if (!m_posterReady) + qWarning(wpLog) + << "WallpaperService: poster decode failed for" << path; + }); + + wake(); +} + void WallpaperService::invalidateCache() { { std::lock_guard lock(m_mutex); @@ -1456,6 +2022,11 @@ QRectF WallpaperService::sourceRect( } void WallpaperService::drawSurface(WallpaperSurface* surface) { + if (!m_transitions.empty()) { + drawSurfaceTransition(surface); + return; + } + if (!surface->needsDraw() || !surface->frameReady()) return; EGLSurface target = surface->eglSurface(); @@ -1505,11 +2076,9 @@ void WallpaperService::drawSurface(WallpaperSurface* surface) { #ifdef ZHAVE_VIDEO const bool videoAsBackground = - (videoNow || (m_previousVideo && m_fading)) && - (m_mpv != nullptr || m_player != nullptr); + videoNow && (m_mpv != nullptr || m_player != nullptr); #else - const bool videoAsBackground = - (videoNow || (m_previousVideo && m_fading)) && m_mpv != nullptr; + const bool videoAsBackground = videoNow && m_mpv != nullptr; #endif if (videoAsBackground) { @@ -1568,107 +2137,13 @@ void WallpaperService::drawSurface(WallpaperSurface* surface) { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - if (m_fading && !m_fadeClockStarted) { - m_fadeClockStarted = true; - m_fadeTimer.start(); - } - - bool fade = false; - bool fadeDone = false; - qreal progress = 1.0; - - if (m_fading) { - progress = m_fadeClockStarted ? qreal(m_fadeTimer.elapsed()) / - qreal(kFadeDurationMs) - : 0.0; - - if (progress >= 1.0 && m_replacementReady) - fadeDone = true; - else - fade = true; - } - - const float previousAlpha = m_replacementReady ? float(1.0 - progress) - : 1.0f; - const float replacementAlpha = m_replacementReady ? float(progress) - : 0.0f; - const float w = float(size.width()); const float h = float(size.height()); - if (videoNow) { - if (fade && !m_previousImage.isNull()) { - if (m_previousTexture == 0) - uploadTexture(m_previousTexture, m_previousImage); - - if (m_previousTexture != 0) { - const QRectF previousRect = sourceRect( - m_previousCrops, - surface->screen(), - size, - m_previousImage.size()); - - drawTexture( - m_previousTexture, previousRect, w, h, previousAlpha); - } - } else if (fade && !m_firstFadeDone) { - if (m_blackTex == 0) createBlackTexture(m_blackTex); - - if (m_blackTex != 0) - drawTexture( - m_blackTex, - QRectF(0.0, 0.0, 1.0, 1.0), - w, - h, - previousAlpha); - } - } else { + if (!videoNow) { const QRectF rect = sourceRect(crops, surface->screen(), size, m_image.size()); - - if (fade) { - if (!m_previousVideo && !m_previousImage.isNull() && - m_previousTexture == 0) { - uploadTexture(m_previousTexture, m_previousImage); - } - - if (!m_previousVideo && m_previousTexture != 0) { - const QRectF previousRect = sourceRect( - m_previousCrops, - surface->screen(), - size, - m_previousImage.size()); - - drawTexture(m_previousTexture, previousRect, w, h, 1.0f); - } - - drawTexture(m_texture, rect, w, h, replacementAlpha); - } else { - drawTexture(m_texture, rect, w, h, 1.0f); - } - } - - if (fadeDone) { - m_fading = false; - m_firstFadeDone = true; - - if (m_previousVideo) { - teardownMpv(); -#ifdef ZHAVE_VIDEO - if (m_player) { - m_player->deleteTexture(); - stopPlayerVideo(); - } -#endif - m_previousVideo = false; - } - - if (m_previousTexture != 0) { - glDeleteTextures(1, &m_previousTexture); - m_previousTexture = 0; - } - - m_previousImage = QImage(); + drawTexture(m_texture, rect, w, h, 1.0f); } } @@ -1681,7 +2156,560 @@ void WallpaperService::drawSurface(WallpaperSurface* surface) { return; } - if ((m_isVideo || m_previousVideo) && m_mpvRender) + if (m_isVideo && m_mpvRender) mpv_render_context_report_swap(m_mpvRender); + + surface->endDraw(); +} + +void WallpaperService::ensureBaseFbo() { + if (m_baseFbo != 0) return; + + glGenFramebuffers(1, &m_baseFbo); + glGenTextures(1, &m_baseTex); + + glBindTexture(GL_TEXTURE_2D, m_baseTex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); +} + +void WallpaperService::ensureAccum(const QSize& size) { + if (m_accumFbo == 0) { + glGenFramebuffers(1, &m_accumFbo); + glGenTextures(1, &m_accumA); + glGenTextures(1, &m_accumB); + + for (uint32_t tex : {m_accumA, m_accumB}) { + glBindTexture(GL_TEXTURE_2D, tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + } + + if (m_accumSize != size) { + for (uint32_t tex : {m_accumA, m_accumB}) { + glBindTexture(GL_TEXTURE_2D, tex); + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RGBA8, + size.width(), + size.height(), + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + nullptr); + } + m_accumSize = size; + } +} + +void WallpaperService::bindRtt(uint32_t fbo, uint32_t tex, const QSize& size) { + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture2D( + GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); + glViewport(0, 0, size.width(), size.height()); +} + +void WallpaperService::drawTransitionPair( + const TransProgram& program, + uint32_t fromTex, + float fromFlip, + const QRectF& fromCrop, + uint32_t toTex, + float toFlip, + const QRectF& toCrop, + qreal progress, + const QSize& size) { + glUseProgram(program.id); + glDisable(GL_BLEND); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, fromTex); + glUniform1i(GLint(program.from), 0); + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, toTex); + glUniform1i(GLint(program.to), 1); + + glUniform2f(GLint(program.size), float(size.width()), float(size.height())); + glUniform4f( + GLint(program.fromCrop), + GLfloat(fromCrop.x()), + GLfloat(fromCrop.y()), + GLfloat(fromCrop.width()), + GLfloat(fromCrop.height())); + glUniform4f( + GLint(program.toCrop), + GLfloat(toCrop.x()), + GLfloat(toCrop.y()), + GLfloat(toCrop.width()), + GLfloat(toCrop.height())); + glUniform1f(GLint(program.fromFlip), fromFlip); + glUniform1f(GLint(program.toFlip), toFlip); + glUniform1f(GLint(program.progress), float(progress)); + + glDrawArrays(GL_TRIANGLES, 0, 3); +} + +uint32_t WallpaperService::targetTexFor( + const TransitionLayer& layer, int index) const { + if (layer.isVideo) return m_captureTex; + if (layer.tex != 0) return layer.tex; + Q_UNUSED(index); + return m_texture; +} + +QRectF WallpaperService::cropForLayer( + const TransitionLayer& layer, + const QString& screen, + const QSize& size) const { + if (layer.isVideo) return QRectF(0.0, 0.0, 1.0, 1.0); + if (layer.tex != 0) + return sourceRect(layer.crops, screen, size, layer.image.size()); + return sourceRect(m_crops, screen, size, m_image.size()); +} + +void WallpaperService::captureBase( + const QSize& size, + const QString& screen, + uint32_t oldTex, + float oldFlip, + const QRectF& oldCrop) { + Q_UNUSED(screen); + + ensureBaseFbo(); + if (m_baseTex == 0) return; + + Q_UNUSED(oldCrop); + if (!m_baseImage.isNull()) { + uploadTexture(m_baseTex, m_baseImage); + m_baseSize = m_baseImage.size(); + m_baseReady = true; + return; + } + + if (m_baseSize != size) { + glBindTexture(GL_TEXTURE_2D, m_baseTex); + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RGBA8, + size.width(), + size.height(), + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + nullptr); + m_baseSize = size; + } + + bindRtt(m_baseFbo, m_baseTex, size); + drawTransitionPair( + m_neutralProgram, + oldTex, + oldFlip, + QRectF(0.0, 0.0, 1.0, 1.0), + oldTex, + oldFlip, + QRectF(0.0, 0.0, 1.0, 1.0), + 1.0, + size); + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + m_baseReady = true; +} + +void WallpaperService::renderComposite( + const QSize& size, + const QString& screen, + uint32_t outFbo, + uint32_t outTex, + int first, + int last, + uint32_t videoTex, + uint32_t posterTex) { + if (m_baseTex == 0) return; + + ensureAccum(size); + + float fromFlip = 0.0f; + QRectF fromCrop = QRectF(0.0, 0.0, 1.0, 1.0); + if (!m_baseImage.isNull()) { + fromFlip = 1.0f; + fromCrop = sourceRect(m_baseCrops, screen, size, m_baseImage.size()); + } + + bindRtt(m_accumFbo, m_accumA, size); + drawTransitionPair( + m_neutralProgram, + m_baseTex, + fromFlip, + fromCrop, + m_baseTex, + fromFlip, + fromCrop, + 1.0, + size); + + uint32_t read = m_accumA; + uint32_t write = m_accumB; + int drawn = 0; + + for (int i = first; i < last; ++i) { + const TransitionLayer& layer = m_transitions[size_t(i)]; + if (!layer.ready) continue; + + uint32_t toTex = 0; + float toFlip = 1.0f; + QRectF toCrop(0.0, 0.0, 1.0, 1.0); + + if (layer.isVideo) { + if (m_videoReady.load(std::memory_order_relaxed) && videoTex != 0) { + toTex = videoTex; + toFlip = 0.0f; + } else if (posterTex != 0) { + toTex = posterTex; + toFlip = 1.0f; + toCrop = + sourceRect(layer.crops, screen, size, m_posterImage.size()); + } else { + toTex = videoTex; + toFlip = 0.0f; + } + } else { + toTex = targetTexFor(layer, i); + toCrop = cropForLayer(layer, screen, size); + } + + if (toTex == 0) continue; + + const TransProgram program = layer.transitionFile.isEmpty() + ? m_neutralProgram + : programFor(layer.transitionFile); + + bindRtt(m_accumFbo, write, size); + drawTransitionPair( + program, + read, + 0.0f, + QRectF(0.0, 0.0, 1.0, 1.0), + toTex, + toFlip, + toCrop, + layer.progress, + size); + + std::swap(read, write); + ++drawn; + } + + if (drawn == 0) { + if (outFbo == m_baseFbo) return; + read = m_accumA; + } + + if (outFbo == 0) { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, size.width(), size.height()); + } else { + bindRtt(outFbo, outTex, size); + } + + drawTransitionPair( + m_neutralProgram, + read, + 0.0f, + QRectF(0.0, 0.0, 1.0, 1.0), + read, + 0.0f, + QRectF(0.0, 0.0, 1.0, 1.0), + 1.0, + size); +} + +void WallpaperService::drawSurfaceTransition(WallpaperSurface* surface) { + if (!surface->needsDraw() || !surface->frameReady()) return; + + EGLSurface target = surface->eglSurface(); + if (m_context == EGL_NO_CONTEXT || target == EGL_NO_SURFACE || + surface->bufferSize().isEmpty()) + return; + + if (!eglMakeCurrent(m_eglDisplay, target, target, m_context)) { + qWarning(wpLog) << "WallpaperService: the context cannot be made " + "current, eglGetError = 0x" + << Qt::hex << unsigned(eglGetError()); + return; + } + + eglSwapInterval(m_eglDisplay, 0); + + if (m_vao == 0) { + glGenVertexArrays(1, &m_vao); + glBindVertexArray(m_vao); + } + + struct ContextRelease { + EGLDisplay display; + ~ContextRelease() { + eglMakeCurrent( + display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + } contextRelease{m_eglDisplay}; + + { + std::lock_guard lock(m_mutex); + + const QSize size = surface->bufferSize(); + if (size.isEmpty()) return; + const QString screen = surface->screen(); + + if (m_transitions.empty()) return; + + if (!ensureTransitionProgram()) return; + if (m_blackTex == 0) createBlackTexture(m_blackTex); + + ensureBaseFbo(); + + const bool newestVideoLive = + !m_transitions.empty() && m_transitions.back().isVideo && + m_videoReady.load(std::memory_order_relaxed); + + bool anyVideoLayer = false; + for (const auto& layer : m_transitions) + if (layer.isVideo) { + anyVideoLayer = true; + break; + } + + bool needVideo = + (m_needCaptureBase && m_baseIsVideo) || + (m_videoReady.load(std::memory_order_relaxed) && anyVideoLayer); + + const uint32_t videoTex = needVideo ? renderVideoIntoTexture(size) : 0; + + if (m_needCaptureBase && m_baseIsVideo && videoTex == 0) return; + + if (m_needCaptureBase) { + uint32_t oldTex = 0; + float oldFlip = 0.0f; + QRectF oldCrop(0.0, 0.0, 1.0, 1.0); + + if (m_baseIsBlack) { + oldTex = m_blackTex; + oldFlip = 1.0f; + } else if (m_baseIsVideo) { + oldTex = videoTex; + oldFlip = 0.0f; + } else { + oldTex = m_texture; // still the previous image here + oldFlip = 1.0f; + oldCrop = + sourceRect(m_baseCrops, screen, size, m_baseImage.size()); + } + + if (oldTex != 0) + captureBase(size, screen, oldTex, oldFlip, oldCrop); + } + + if (!m_transitions.back().isVideo) { + const bool upload = m_imageChanged; + m_imageChanged = false; + if (m_texture == 0 || upload) uploadImage(); + } else { + m_imageChanged = false; + } + + if (m_isVideo && m_posterChanged) { + m_posterChanged = false; + if (!m_posterImage.isNull()) + uploadTexture(m_posterTex, m_posterImage); + } + + const bool newestVideoPoster = newestVideoLive == false && + m_transitions.back().isVideo && + m_posterReady && m_posterTex != 0; + + const bool newestReady = + m_isVideo + ? (newestVideoLive || newestVideoPoster) + : (!m_image.isNull() && m_texture != 0 && m_replacementReady); + + for (size_t i = 0; i < m_transitions.size(); ++i) { + TransitionLayer& layer = m_transitions[i]; + const bool ready = + (i + 1 == m_transitions.size()) + ? newestReady + : (layer.isVideo + ? (layer.ready || m_videoReady || m_posterReady) + : layer.tex != 0); + if (!ready) continue; + + layer.ready = true; + if (!layer.started) { + layer.started = true; + layer.timer.start(); + } + layer.progress = + qreal(layer.timer.elapsed()) / qreal(kFadeDurationMs); + if (layer.progress > 1.0) layer.progress = 1.0; + } + + const bool newestSettled = + m_transitions.back().isVideo ? newestVideoLive : true; + + const bool collapse = m_collapseVideo && m_transitions.size() > 1; + const bool promote = m_promoteOldest && !m_transitions.empty(); + + if (collapse) { + ensureAccum(size); + renderComposite( + size, + screen, + m_baseFbo, + m_baseTex, + 0, + int(m_transitions.size()) - 1, + videoTex, + m_posterTex); + } else if (promote) { + ensureAccum(size); + renderComposite( + size, screen, m_baseFbo, m_baseTex, 0, 1, videoTex, m_posterTex); + } + + int first = 0; + if (collapse) + first = int(m_transitions.size()) - 1; + else if (promote) + first = 1; + + const int last = int(m_transitions.size()); + const TransitionLayer& newest = m_transitions.back(); + + if (m_baseIsBlack) { + const qreal p = newest.ready ? newest.progress : 0.0; + const float a = p < 0.0 ? 0.0f : (p > 1.0 ? 1.0f : float(p)); + const float w = float(size.width()); + const float h = float(size.height()); + + if (m_program == 0) m_program = createProgram(); + if (m_program == 0) return; + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, size.width(), size.height()); + + if (m_isVideo) { + if (!m_videoReady.load(std::memory_order_relaxed)) { + if (!m_posterReady || m_posterTex == 0) return; + + glUseProgram(m_program); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + drawTexture( + m_posterTex, + sourceRect(m_crops, screen, size, m_posterImage.size()), + w, + h, + a); + } else { + glDisable(GL_BLEND); + +#ifdef ZHAVE_VIDEO + if (m_player) { + if (m_player->textureId() != 0) { + if (m_videoProgram == 0) + m_videoProgram = createVideoProgram(); + + if (m_videoProgram != 0) { + glUseProgram(m_videoProgram); + const QSize src = m_player->frameSize(); + drawVideoTexture( + m_player->textureId(), + m_player->chromaTextureId(), + m_player->colorMatrix(), + m_player->colorRange(), + QRectF(0.0, 0.0, 1.0, 1.0), + w, + h, + src.width() > 0 ? float(src.width()) : w, + src.height() > 0 ? float(src.height()) : h, + 1.0f); + } + } + } else +#endif + { + if (!m_mpvRender) initMpvRender(); + if (!m_mpvRender) return; + renderMpvToSurface(size); + } + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glUseProgram(m_program); + + if (m_blackTex != 0) + drawTexture( + m_blackTex, + QRectF(0.0, 0.0, 1.0, 1.0), + w, + h, + 1.0f - a); + } + } else { + if (m_texture == 0 || m_image.isNull()) return; + + glUseProgram(m_program); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + drawTexture( + m_texture, + sourceRect(m_crops, screen, size, m_image.size()), + w, + h, + a); + } + + if (newest.ready && newest.progress >= 1.0 && newestSettled) + m_newestDone = true; + } else { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, size.width(), size.height()); + + if (!m_baseReady) { + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + } else { + renderComposite( + size, screen, 0, 0, first, last, videoTex, m_posterTex); + } + + if (newest.ready && newest.progress >= 1.0 && newestSettled) + m_newestDone = true; + } + } + + surface->requestFrame(); + + if (!eglSwapBuffers(m_eglDisplay, target)) { + qWarning(wpLog) + << "WallpaperService: eglSwapBuffers failed, eglGetError = 0x" + << QString::number(unsigned(eglGetError()), 16); + return; + } + + if ((m_isVideo || m_baseIsVideo) && m_mpvRender) mpv_render_context_report_swap(m_mpvRender); surface->endDraw(); diff --git a/Plugins/ZShell/Internal/wallpaperservice.hpp b/Plugins/ZShell/Internal/wallpaperservice.hpp index 25cbd65..dad41d8 100644 --- a/Plugins/ZShell/Internal/wallpaperservice.hpp +++ b/Plugins/ZShell/Internal/wallpaperservice.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -15,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +30,7 @@ #include #include "wallpapersurface.hpp" +#include "config/enums.hpp" struct mpv_handle; struct mpv_render_context; @@ -131,8 +134,76 @@ class WallpaperService : public QObject { void storeGlobal(const char* interface, void* proxy); + struct TransProgram { + uint32_t id = 0; + int size = -1; + int progress = -1; + int from = -1; + int to = -1; + int fromCrop = -1; + int toCrop = -1; + int fromFlip = -1; + int toFlip = -1; + }; + [[nodiscard]] bool setupGl(); [[nodiscard]] uint32_t createProgram(); + [[nodiscard]] TransProgram createTransitionProgram(const QString& file); + [[nodiscard]] TransProgram programFor(const QString& file); + bool ensureTransitionProgram(); + QString transitionFileFor(ZShell::config::WallpaperTransition::Enum value); + [[nodiscard]] QString randomTransitionFile(); + + void ensureCapture(const QSize& size); + uint32_t renderVideoIntoTexture(const QSize& size); + + void drawSurfaceTransition(WallpaperSurface* surface); + + struct TransitionLayer { + uint32_t tex = 0; // frozen image target (older layers) + QImage image; // CPU copy for crops/sizing (image layers) + QVariantMap crops; // crop map captured when the layer was pushed + QString transitionFile; // this layer's own transition shader + bool isVideo = false; // only the newest layer may be video + bool ready = false; // target is sampleable + bool started = false; // its own clock has begun + QElapsedTimer timer; + qreal progress = 0.0; + }; + + void ensureBaseFbo(); + void ensureAccum(const QSize& size); + void bindRtt(uint32_t fbo, uint32_t tex, const QSize& size); + void drawTransitionPair( + const TransProgram& program, + uint32_t fromTex, + float fromFlip, + const QRectF& fromCrop, + uint32_t toTex, + float toFlip, + const QRectF& toCrop, + qreal progress, + const QSize& size); + uint32_t targetTexFor(const TransitionLayer& layer, int index) const; + QRectF cropForLayer( + const TransitionLayer& layer, + const QString& screen, + const QSize& size) const; + void captureBase( + const QSize& size, + const QString& screen, + uint32_t oldTex, + float oldFlip, + const QRectF& oldCrop); + void renderComposite( + const QSize& size, + const QString& screen, + uint32_t outFbo, + uint32_t outTex, + int first, + int last, + uint32_t videoTex, + uint32_t posterTex); void uploadTexture(uint32_t& texture, const QImage& image); void uploadImage(); @@ -150,8 +221,14 @@ class WallpaperService : public QObject { [[nodiscard]] QByteArray cachedBlob(const QString& path) const; + [[nodiscard]] QByteArray posterBlob(const QString& path) const; + + [[nodiscard]] QVariantMap cropsForPath(const QString& path) const; + void ensureImage(); + void ensurePoster(); + void notifyBufferSizes(); void queue(WallpaperSurface* surface); @@ -187,7 +264,6 @@ class WallpaperService : public QObject { bool m_isVideo = false; bool m_videoReload = false; - bool m_previousVideo = false; mpv_handle* m_mpv = nullptr; mpv_render_context* m_mpvRender = nullptr; uint32_t m_blackTex = 0; @@ -223,6 +299,45 @@ class WallpaperService : public QObject { int m_alphaUniform = -1; int m_sourceUniform = -1; + bool m_randomTransition = false; // wallTransition == Random + QString m_transitionFile; // the transition stamped onto new layers + bool m_transitionDirty = false; + QHash + m_transPrograms; // compiled programs, keyed by shader + TransProgram + m_neutralProgram; // fade program for identity copies (seed/base/final) + QVector + m_transitionUses; // usage counts, parallel to the transition list + QString m_lastTransition; // last random pick, so it is never repeated + + uint32_t m_captureFbo = 0; + uint32_t m_captureTex = 0; + QSize m_captureSize; + + uint32_t m_baseFbo = 0; + uint32_t m_baseTex = 0; + QSize m_baseSize; + bool m_baseReady = false; + + uint32_t m_accumFbo = 0; + uint32_t m_accumA = 0; + uint32_t m_accumB = 0; + QSize m_accumSize; + + std::vector m_transitions; // oldest .. newest + + bool m_needCaptureBase = false; + QImage m_baseImage; // old settled image, for the base capture crop + QVariantMap m_baseCrops; // old settled crop map, for the base capture crop + bool m_baseIsVideo = false; // old settled frame was a live video + bool m_baseIsBlack = false; // shell start / first load: fade in from black + bool m_collapseVideo = + false; // video pushed mid-stack: bake older into base + bool m_promoteOldest = false; // stack over the cap: promote oldest to base + bool m_newestDone = false; // newest layer finished: settle into base + + static constexpr int kMaxTransitions = 4; + uint32_t m_videoProgram = 0; uint32_t m_vao = 0; int m_videoSizeUniform = -1; @@ -240,14 +355,7 @@ class WallpaperService : public QObject { bool m_usePlayer = true; #endif - QImage m_previousImage; - QVariantMap m_previousCrops; - uint32_t m_previousTexture = 0; std::vector m_orphanTextures; - bool m_fading = false; - bool m_fadeClockStarted = false; - bool m_firstFadeDone = false; - QElapsedTimer m_fadeTimer; QList m_surfaces; @@ -264,6 +372,13 @@ class WallpaperService : public QObject { bool m_forceDecode = false; bool m_imageChanged = false; + + QImage m_posterImage; + uint32_t m_posterTex = 0; + QString m_posterPath; + bool m_posterReady = false; + bool m_posterLoading = false; + bool m_posterChanged = false; }; } // namespace ZShell::internal diff --git a/Plugins/ZShell/Models/wallpaperdb.cpp b/Plugins/ZShell/Models/wallpaperdb.cpp index b69fd50..ace438d 100644 --- a/Plugins/ZShell/Models/wallpaperdb.cpp +++ b/Plugins/ZShell/Models/wallpaperdb.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include #include @@ -343,8 +345,8 @@ void WallpaperDb::setCacheSize(const QSize& cacheSize) { if (db.isOpen()) { QSqlQuery query(db); query.prepare( - "UPDATE wallpapers SET cache = NULL, scheme_thumb = NULL" - " WHERE path = ?"); + "UPDATE wallpapers SET cache = NULL, scheme_thumb = NULL," + " poster = NULL WHERE path = ?"); query.addBindValue(entry->path()); query.exec(); } @@ -413,7 +415,8 @@ void WallpaperDb::openDb() { query.exec( "CREATE TABLE IF NOT EXISTS wallpapers(" "path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER," - " size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB)"); + " size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB," + " poster BLOB, crop TEXT)"); query.exec("SELECT cache FROM wallpapers LIMIT 1"); if (query.lastError().isValid()) { @@ -421,9 +424,12 @@ void WallpaperDb::openDb() { query.exec( "CREATE TABLE wallpapers(" "path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER," - " size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB)"); + " size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB," + " poster BLOB, crop TEXT)"); } else { query.exec("ALTER TABLE wallpapers ADD COLUMN scheme_thumb BLOB"); + query.exec("ALTER TABLE wallpapers ADD COLUMN poster BLOB"); + query.exec("ALTER TABLE wallpapers ADD COLUMN crop TEXT"); } registerProvider(); @@ -538,6 +544,7 @@ void WallpaperDb::applyScan(const ScanResult& result) { } delete it.value(); + m_crops.remove(it.key()); it = m_byPath.erase(it); } else { ++it; @@ -575,24 +582,28 @@ void WallpaperDb::applyScan(const ScanResult& result) { if (db.isOpen()) { QSqlQuery query(db); query.prepare( - "SELECT cache, scheme_thumb, size, mtime FROM wallpapers" - " WHERE path = ?"); + "SELECT cache, scheme_thumb, poster, size, mtime, crop" + " FROM wallpapers WHERE path = ?"); query.addBindValue(path); query.exec(); if (query.next()) { const auto cache = query.value(0).toByteArray(); const auto scheme = query.value(1).toByteArray(); - const auto storedSize = query.value(2).toLongLong(); - const auto storedMtime = query.value(3).toLongLong(); + const auto poster = query.value(2).toByteArray(); + const auto storedSize = query.value(3).toLongLong(); + const auto storedMtime = query.value(4).toLongLong(); + loadCrop(path, query.value(5).toString()); + + const bool posterOk = !isVideo || !poster.isEmpty(); if (storedSize == size && storedMtime == mtime && !cache.isEmpty()) { reused = true; entry->setThumbnailPath(thumbnailUrl(path)); - if (scheme.isEmpty()) { - queueMetadata(entry, isVideo, cache); + if (scheme.isEmpty() || !posterOk) { + queueMetadata(entry, isVideo, cache, poster); } } } @@ -602,9 +613,13 @@ void WallpaperDb::applyScan(const ScanResult& result) { if (db.isOpen()) { QSqlQuery query(db); query.prepare( - "INSERT OR REPLACE INTO wallpapers" + "INSERT INTO wallpapers" " (path, kind, hardware, size, mtime, cache)" - " VALUES (?, ?, ?, ?, ?, NULL)"); + " VALUES (?, ?, ?, ?, ?, NULL)" + " ON CONFLICT(path) DO UPDATE SET kind = excluded.kind," + " hardware = excluded.hardware, size = excluded.size," + " mtime = excluded.mtime, cache = NULL," + " scheme_thumb = NULL, poster = NULL"); query.addBindValue(path); query.addBindValue(isVideo ? "video" : "image"); query.addBindValue( @@ -626,7 +641,10 @@ void WallpaperDb::applyScan(const ScanResult& result) { } void WallpaperDb::queueMetadata( - WallpaperEntry* entry, bool isVideo, const QByteArray& existingCache) { + WallpaperEntry* entry, + bool isVideo, + const QByteArray& existingCache, + const QByteArray& existingPoster) { qCInfo( dbLog, "WallpaperDb: queued cache job for %s (%s%s)", @@ -634,7 +652,8 @@ void WallpaperDb::queueMetadata( isVideo ? "video" : "image", existingCache.isEmpty() ? "" : ", existing blob"); - m_queue.append(MetadataJob{entry, entry->path(), isVideo, existingCache}); + m_queue.append( + MetadataJob{entry, entry->path(), isVideo, existingCache, existingPoster}); if (!m_queueTimer.isActive()) { m_queueTimer.start(); @@ -672,10 +691,12 @@ void WallpaperDb::runMetadataJob(const MetadataJob& job) { struct CacheResult { QString path; QByteArray cache; + QByteArray poster; QByteArray scheme; }; const QSize cacheSize = m_cacheSize; + const bool isVideoJob = job.isVideo; QElapsedTimer clock; clock.start(); @@ -685,7 +706,7 @@ void WallpaperDb::runMetadataJob(const MetadataJob& job) { watcher, &QFutureWatcher::finished, this, - [this, watcher, clock]() { + [this, watcher, clock, isVideoJob]() { const auto result = watcher->result(); qCInfo( @@ -695,15 +716,27 @@ void WallpaperDb::runMetadataJob(const MetadataJob& job) { int(result.cache.size()), int(clock.elapsed())); + qCInfo( + dbLog, + "WallpaperDb: %s poster %d bytes", + result.path.toUtf8().constData(), + int(result.poster.size())); + + if (isVideoJob && result.poster.isEmpty()) { + qWarning(dbLog) + << "WallpaperDb: no poster produced for" << result.path; + } + if (QSqlDatabase::contains(m_uuid)) { const auto db = QSqlDatabase::database(m_uuid, false); if (db.isOpen()) { QSqlQuery query(db); query.prepare( - "UPDATE wallpapers SET cache = ?, scheme_thumb = ?" - " WHERE path = ?"); + "UPDATE wallpapers SET cache = ?, scheme_thumb = ?," + " poster = ? WHERE path = ?"); query.addBindValue(result.cache); query.addBindValue(result.scheme); + query.addBindValue(result.poster); query.addBindValue(result.path); if (!query.exec()) { qWarning(dbLog) @@ -736,7 +769,14 @@ void WallpaperDb::runMetadataJob(const MetadataJob& job) { : imageCacheBytes(job.path, cacheSize); } + QByteArray poster = job.existingPoster; + if (poster.isEmpty()) { + poster = + job.isVideo ? videoPosterBytes(job.path, cacheSize) : cache; + } + result.cache = cache; + result.poster = poster; result.scheme = cache.isEmpty() ? QByteArray() : schemeThumbBytes(cache, 128); @@ -781,4 +821,56 @@ bool WallpaperDb::entryAllowed(const WallpaperEntry* entry) const { return entry->hardware(); } +QVariantMap WallpaperDb::crop(const QString& path, const QString& screen) const { + const auto pathCrops = m_crops.constFind(path); + if (pathCrops == m_crops.constEnd()) return {}; + return pathCrops.value().value(screen).toMap(); +} + +void WallpaperDb::setCrop( + const QString& path, const QString& screen, const QVariantMap& crop) { + if (path.isEmpty() || screen.isEmpty()) return; + + QVariantMap& pathCrops = m_crops[path]; + pathCrops[screen] = crop; + storeCropJson(path); + emit cropsChanged(path); +} + +void WallpaperDb::loadCrop(const QString& path, const QString& json) { + if (json.isEmpty()) { + m_crops.remove(path); + return; + } + + const auto doc = QJsonDocument::fromJson(json.toUtf8()); + if (!doc.isObject()) { + m_crops.remove(path); + return; + } + + m_crops[path] = doc.object().toVariantMap(); +} + +void WallpaperDb::storeCropJson(const QString& path) { + const auto db = QSqlDatabase::database(m_uuid, false); + if (!db.isOpen()) return; + + const auto pathCrops = m_crops.value(path); + const QByteArray json = + pathCrops.isEmpty() + ? QByteArray() + : QJsonDocument(QJsonObject::fromVariantMap(pathCrops)) + .toJson(QJsonDocument::Compact); + + QSqlQuery query(db); + query.prepare("UPDATE wallpapers SET crop = ? WHERE path = ?"); + query.addBindValue(json.isEmpty() ? QVariant() : QVariant(json)); + query.addBindValue(path); + if (!query.exec()) { + qWarning(dbLog) << "WallpaperDb: could not store the crop for" << path + << query.lastError(); + } +} + } // namespace ZShell::models diff --git a/Plugins/ZShell/Models/wallpaperdb.hpp b/Plugins/ZShell/Models/wallpaperdb.hpp index 30592e4..e41053d 100644 --- a/Plugins/ZShell/Models/wallpaperdb.hpp +++ b/Plugins/ZShell/Models/wallpaperdb.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include namespace ZShell::models { @@ -109,6 +110,11 @@ class WallpaperDb : public QObject { [[nodiscard]] static QString thumbnailUrl(const QString& path); + Q_INVOKABLE QVariantMap crop(const QString& path, const QString& screen) + const; + Q_INVOKABLE void setCrop( + const QString& path, const QString& screen, const QVariantMap& crop); + signals: void pathChanged(); void dbPathChanged(); @@ -117,6 +123,7 @@ class WallpaperDb : public QObject { void cacheSizeChanged(); void cacheStored(const QString& path); + void cropsChanged(const QString& path); private: struct FileEntry { @@ -134,6 +141,7 @@ class WallpaperDb : public QObject { QString path; bool isVideo; QByteArray existingCache; + QByteArray existingPoster; }; void openDb(); @@ -144,11 +152,15 @@ class WallpaperDb : public QObject { void queueMetadata( WallpaperEntry* entry, bool isVideo, - const QByteArray& existingCache = {}); + const QByteArray& existingCache = {}, + const QByteArray& existingPoster = {}); void processQueue(); void runMetadataJob(const MetadataJob& job); void rebuildEntries(); + void loadCrop(const QString& path, const QString& json); + void storeCropJson(const QString& path); + [[nodiscard]] bool entryAllowed(const WallpaperEntry* entry) const; QDir m_dir; @@ -163,6 +175,8 @@ class WallpaperDb : public QObject { QList m_entries; QHash m_byPath; + QHash m_crops; + QVector m_queue; QTimer m_queueTimer; bool m_jobRunning = false; diff --git a/Plugins/ZShell/Models/wallpaperutil.hpp b/Plugins/ZShell/Models/wallpaperutil.hpp index d5b3a13..447fc67 100644 --- a/Plugins/ZShell/Models/wallpaperutil.hpp +++ b/Plugins/ZShell/Models/wallpaperutil.hpp @@ -285,6 +285,91 @@ inline QByteArray videoCacheBytes(const QString& src, const QSize& fit) { return frame.readAll(); } +inline QByteArray videoPosterBytes(const QString& src, const QSize& fit) { + auto runProcess = [](const QString& program, const QStringList& args) { + QProcess process; + process.start(program, args); + if (!process.waitForFinished(15000)) { + process.kill(); + process.waitForFinished(1000); + } + return qMakePair(process.exitCode(), process.readAllStandardOutput()); + }; + + QSize source; + const auto resProbe = runProcess( + "ffprobe", + { + "-v", + "error", + "-select_streams", + "v:0", + "-show_entries", + "stream=width,height", + "-of", + "csv=s=x:p=0", + src, + }); + if (resProbe.first == 0) { + const auto parts = + QString::fromUtf8(resProbe.second).trimmed().split('x'); + if (parts.size() == 2) { + source = QSize(parts[0].toInt(), parts[1].toInt()); + } + } + + QSize target = coverSizeNoUpscale(source, fit); + if (target.isEmpty()) { + target = fit; + } + + target.setWidth(target.width() & ~1); + target.setHeight(target.height() & ~1); + + const QString scale = + QStringLiteral( + "scale=%1:%2:force_original_aspect_ratio=decrease," + "scale=trunc(iw/2)*2:trunc(ih/2)*2") + .arg(target.width()) + .arg(target.height()); + + QTemporaryDir tmpDir; + if (!tmpDir.isValid()) { + return {}; + } + + const QString framePath = tmpDir.filePath(QStringLiteral("poster.jpg")); + + runProcess( + "ffmpeg", + { + "-hide_banner", + "-loglevel", + "error", + "-ss", + "0", + "-i", + src, + "-frames:v", + "1", + "-vf", + scale, + "-pix_fmt", + "yuvj420p", + "-q:v", + "2", + "-y", + framePath, + }); + + QFile frame(framePath); + if (!frame.open(QIODevice::ReadOnly)) { + return {}; + } + + return frame.readAll(); +} + inline QByteArray schemeThumbBytes(const QByteArray& source, int maxSide) { QImage image = QImage::fromData(source, "JPEG"); if (image.isNull()) {