feat: add wallpaper transitions, store crop in database per-wallpaper, move CUDA context creation to async thread
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 28s
Python / verify (pull_request) Failing after 1m29s
Rust / build (pull_request) Successful in 53s
Rust / fmt (pull_request) Successful in 42s
Rust / clippy (pull_request) Successful in 1m4s

This commit is contained in:
2026-10-26 14:47:47 +01:00
parent dc0f222ace
commit e655ec5bc7
26 changed files with 2338 additions and 210 deletions
+6 -1
View File
@@ -1,7 +1,10 @@
#pragma once
#include "common.hpp"
#include <qqmlintegration.h>
#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
+13
View File
@@ -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
+11
View File
@@ -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)
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
+31 -11
View File
@@ -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<std::mutex> 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();
+2 -1
View File
@@ -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<bool> m_threadRunning{false};
std::atomic<uint32_t> m_generation{0};
mutable std::mutex m_mtx;
AVFrame* m_pending = nullptr;
+39 -1
View File
@@ -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);
@@ -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
File diff suppressed because it is too large Load Diff
+123 -8
View File
@@ -8,6 +8,7 @@
#include <vector>
#include <QImage>
#include <QHash>
#include <QList>
#include <QElapsedTimer>
#include <QRectF>
@@ -15,6 +16,7 @@
#include <QString>
#include <QStringList>
#include <QVariantMap>
#include <QVector>
#include <QtQml/qqml.h>
#include <qtmetamacros.h>
#include <atomic>
@@ -28,6 +30,7 @@
#include <wayland-client-protocol.h>
#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<QString, TransProgram>
m_transPrograms; // compiled programs, keyed by shader
TransProgram
m_neutralProgram; // fade program for identity copies (seed/base/final)
QVector<int>
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<TransitionLayer> 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<uint32_t> m_orphanTextures;
bool m_fading = false;
bool m_fadeClockStarted = false;
bool m_firstFadeDone = false;
QElapsedTimer m_fadeTimer;
QList<WallpaperSurface*> 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
+109 -17
View File
@@ -8,6 +8,8 @@
#include <qfuturewatcher.h>
#include <qimage.h>
#include <qimagereader.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qlogging.h>
#include <qqmlengine.h>
#include <qsqldatabase.h>
@@ -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<CacheResult>::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
+15 -1
View File
@@ -11,6 +11,7 @@
#include <qqmllist.h>
#include <qsize.h>
#include <qtimer.h>
#include <qvariant.h>
#include <qvector.h>
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<WallpaperEntry*> m_entries;
QHash<QString, WallpaperEntry*> m_byPath;
QHash<QString, QVariantMap> m_crops;
QVector<MetadataJob> m_queue;
QTimer m_queueTimer;
bool m_jobRunning = false;
+85
View File
@@ -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()) {