From c7d447c986bcfb94989469f5f0bd1ea0188c8f7b Mon Sep 17 00:00:00 2001 From: Kun Zhang Date: Wed, 1 Jul 2026 09:32:25 +0800 Subject: [PATCH] feat(wallpaper): support shader wallpaper sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add shader as a wallpaper source type and wire it through the wallpaper manager, notifier, persisted config, and wallpaper factory. Shader wallpapers are loaded from precompiled qsb packages and driven by a frame-synchronized clock so animation time follows actual rendered frames. Extend the test wallpaper client to import .frag files by compiling them with qsb, caching shared shader packages under /tmp, and falling back to /usr/lib/qt6/bin/qsb when qsb is not in PATH. Add qt6-shader-baker as the examples runtime dependency. Log: 支持导入并渲染 shader 动态壁纸 --- debian/control | 3 +- examples/test_set_wallpaper/main.cpp | 238 +++++++++++++++++- .../treelandwallpapermanagerclient.h | 2 +- .../wallpaper/wallpapermanagerinterfacev1.cpp | 16 ++ .../wallpaper/wallpapermanagerinterfacev1.h | 6 +- .../wallpapernotifierinterfacev1.cpp | 10 + .../wallpaper/wallpapernotifierinterfacev1.h | 2 +- src/wallpaper/wallpaperconfig.cpp | 21 +- src/wallpaper/wallpapermanager.cpp | 45 ++++ src/wallpaper/wallpapermanager.h | 3 + wallpaper-factory/CMakeLists.txt | 3 + wallpaper-factory/Shader.qml | 81 ++++++ wallpaper-factory/shaderframeclock.cpp | 115 +++++++++ wallpaper-factory/shaderframeclock.h | 52 ++++ .../treelandwallpapernotifierclient.cpp | 58 ++++- 15 files changed, 642 insertions(+), 13 deletions(-) create mode 100644 wallpaper-factory/Shader.qml create mode 100644 wallpaper-factory/shaderframeclock.cpp create mode 100644 wallpaper-factory/shaderframeclock.h diff --git a/debian/control b/debian/control index e4e62b698..cb8009bef 100644 --- a/debian/control +++ b/debian/control @@ -75,7 +75,8 @@ Package: treeland-examples Section: dde Architecture: any Multi-Arch: same -Depends: ${misc:Depends}, +Depends: qt6-shader-baker, + ${misc:Depends}, ${shlibs:Depends}, Description: examples for treeland. diff --git a/examples/test_set_wallpaper/main.cpp b/examples/test_set_wallpaper/main.cpp index c21970580..7149d2462 100644 --- a/examples/test_set_wallpaper/main.cpp +++ b/examples/test_set_wallpaper/main.cpp @@ -12,8 +12,19 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include + static QScreen *findScreenByName(const QString &name) { if (name.isEmpty()) @@ -42,6 +53,210 @@ static bool isVideoFile(const QString &path) return mime.isValid() && mime.name().startsWith("video/"); } +static bool isFragmentShaderFile(const QString &path) +{ + return QFileInfo(path).suffix().compare(QStringLiteral("frag"), Qt::CaseInsensitive) == 0; +} + +static QByteArray shaderSourceWithoutVersion(const QByteArray &source) +{ + QByteArray result; + const QList lines = source.split('\n'); + for (const QByteArray &line : lines) { + if (line.trimmed().startsWith("#version")) + continue; + + result += line; + result += '\n'; + } + return result; +} + +static QByteArray shaderBakeSource(const QByteArray &source) +{ + if (!source.contains("mainImage")) + return source; + + QByteArray baked; + baked += "#version 440\n"; + baked += "layout(location = 0) in vec2 qt_TexCoord0;\n"; + baked += "layout(location = 0) out vec4 fragColor;\n"; + baked += "layout(std140, binding = 0) uniform buf {\n"; + baked += " mat4 qt_Matrix;\n"; + baked += " float qt_Opacity;\n"; + baked += " float iTime;\n"; + baked += " vec2 iResolution;\n"; + baked += " vec4 iMouse;\n"; + baked += "};\n"; + baked += shaderSourceWithoutVersion(source); + baked += "\nvoid main() {\n"; + baked += " vec2 fragCoord = vec2(qt_TexCoord0.x, 1.0 - qt_TexCoord0.y) * iResolution;\n"; + baked += " mainImage(fragColor, fragCoord);\n"; + baked += " fragColor *= qt_Opacity;\n"; + baked += "}\n"; + return baked; +} + +static bool writeFileAtomically(const QString &path, const QByteArray &data) +{ + QSaveFile file(path); + if (!file.open(QIODevice::WriteOnly)) + return false; + + if (file.write(data) != data.size()) + return false; + + if (!file.commit()) + return false; + + QFile::setPermissions(path, + QFileDevice::ReadOwner | QFileDevice::WriteOwner + | QFileDevice::ReadGroup + | QFileDevice::ReadOther); + return true; +} + +static bool ensureSharedDirectory(const QString &path) +{ + QDir dir; + if (!dir.mkpath(path)) + return false; + + return QFile::setPermissions(path, + QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner + | QFileDevice::ReadGroup | QFileDevice::ExeGroup + | QFileDevice::ReadOther | QFileDevice::ExeOther); +} + +static bool hasCurrentShaderPackageVersion(const QString &metadataPath) +{ + QFile file(metadataPath); + if (!file.open(QIODevice::ReadOnly)) + return false; + + const QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); + return doc.isObject() && doc.object()[QStringLiteral("packageVersion")].toInt() == 3; +} + +static QString findQsbExecutable() +{ + const QString qsb = QStandardPaths::findExecutable(QStringLiteral("qsb")); + if (!qsb.isEmpty()) + return qsb; + + const QString qt6Qsb = QStringLiteral("/usr/lib/qt6/bin/qsb"); + if (QFileInfo(qt6Qsb).isExecutable()) + return qt6Qsb; + + return QString(); +} + +static QString shaderPackageRoot() +{ + return QDir::temp().filePath(QStringLiteral("treeland-wallpaper-shader/%1").arg(getuid())); +} + +static bool ensureShaderPackage(const QString &fragmentPath, QString *packagePath) +{ + QFile fragment(fragmentPath); + if (!fragment.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open fragment shader:" << fragmentPath << fragment.errorString(); + return false; + } + + const QByteArray source = fragment.readAll(); + const QByteArray hash = QCryptographicHash::hash(source, QCryptographicHash::Sha256).toHex(); + QDir root(shaderPackageRoot()); + if (!ensureSharedDirectory(root.absolutePath()) + || !ensureSharedDirectory(root.filePath(QString::fromLatin1(hash)))) { + qCritical() << "Failed to create shader wallpaper cache directory:" << root.absolutePath(); + return false; + } + + QDir package(root.filePath(QString::fromLatin1(hash))); + const QString sourcePath = package.filePath(QStringLiteral("fragment.frag")); + const QString bakePath = package.filePath(QStringLiteral("fragment.qt.frag")); + const QString qsbPath = package.filePath(QStringLiteral("fragment.qsb")); + const QString qsbTempPath = package.filePath(QStringLiteral("fragment.qsb.tmp")); + const QString metadataPath = package.filePath(QStringLiteral("metadata.json")); + + if (!QFileInfo::exists(qsbPath) || !hasCurrentShaderPackageVersion(metadataPath)) { + if (!writeFileAtomically(sourcePath, source)) { + qCritical() << "Failed to write shader source:" << sourcePath; + return false; + } + + if (!writeFileAtomically(bakePath, shaderBakeSource(source))) { + qCritical() << "Failed to write shader bake source:" << bakePath; + return false; + } + + const QString qsb = findQsbExecutable(); + if (qsb.isEmpty()) { + qCritical() << "Cannot find qsb. Install qt6-shader-baker to import .frag wallpaper."; + return false; + } + + QFile::remove(qsbTempPath); + + QProcess process; + process.setProgram(qsb); + process.setArguments({ + QStringLiteral("--glsl"), QStringLiteral("100es,120,150"), + QStringLiteral("--hlsl"), QStringLiteral("50"), + QStringLiteral("--msl"), QStringLiteral("12"), + QStringLiteral("-o"), qsbTempPath, + bakePath, + }); + process.start(); + if (!process.waitForFinished(30000)) { + process.kill(); + process.waitForFinished(); + qCritical() << "Timed out compiling fragment shader with qsb:" << bakePath; + QFile::remove(qsbTempPath); + return false; + } + + if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) { + qCritical().noquote() << "Failed to compile fragment shader with qsb:" + << QString::fromLocal8Bit(process.readAllStandardError()); + QFile::remove(qsbTempPath); + return false; + } + + if (!QFileInfo(qsbTempPath).isFile() || QFileInfo(qsbTempPath).size() <= 0) { + qCritical() << "qsb did not produce a valid shader package output:" << qsbTempPath; + QFile::remove(qsbTempPath); + return false; + } + + QFile::remove(qsbPath); + if (!QFile::rename(qsbTempPath, qsbPath)) { + qCritical() << "Failed to install compiled shader package output:" << qsbPath; + QFile::remove(qsbTempPath); + return false; + } + + QJsonObject metadata; + metadata[QStringLiteral("packageVersion")] = 3; + metadata[QStringLiteral("type")] = QStringLiteral("shader"); + metadata[QStringLiteral("fragmentShader")] = QStringLiteral("fragment.qsb"); + metadata[QStringLiteral("sourceShader")] = QStringLiteral("fragment.frag"); + metadata[QStringLiteral("uniforms")] = QJsonObject { + { QStringLiteral("iTime"), true }, + { QStringLiteral("iResolution"), true }, + { QStringLiteral("iMouse"), false }, + }; + if (!writeFileAtomically(metadataPath, QJsonDocument(metadata).toJson(QJsonDocument::Indented))) { + qCritical() << "Failed to write shader wallpaper metadata:" << metadataPath; + return false; + } + } + + *packagePath = package.absolutePath(); + return true; +} + static uint32_t parseRole(const QString &role) { @@ -76,7 +291,7 @@ int main(int argc, char *argv[]) parser.addOption(outputNameOption); QCommandLineOption pathOption( QStringLiteral("path"), - QStringLiteral("Wallpaper path (image or video)."), + QStringLiteral("Wallpaper path (image, video, or fragment shader)."), QStringLiteral("file-path") ); parser.addOption(pathOption); @@ -149,6 +364,27 @@ int main(int argc, char *argv[]) wallpaper->set_image_source(wallpaperPath, role); } else if (isVideoFile(wallpaperPath)) { wallpaper->set_video_source(wallpaperPath, role); + } else if (isFragmentShaderFile(wallpaperPath)) { + if (wallpaper->version() < TreelandWallpaperManagerV1::InterfaceVersion) { + qCritical() << "Compositor wallpaper protocol version is" << wallpaper->version() + << "but shader wallpaper requires version" + << TreelandWallpaperManagerV1::InterfaceVersion + << ". Restart treeland with the updated treeland-protocols and compositor build."; + delete wallpaper; + delete wallpaperManager; + qApp->exit(EXIT_FAILURE); + return; + } + + QString packagePath; + if (!ensureShaderPackage(wallpaperPath, &packagePath)) { + delete wallpaper; + delete wallpaperManager; + qApp->exit(EXIT_FAILURE); + return; + } + + wallpaper->set_shader_source(packagePath, role); } else { qCritical() << "Unsupported wallpaper file type:" << wallpaperPath; delete wallpaper; diff --git a/examples/test_set_wallpaper/treelandwallpapermanagerclient.h b/examples/test_set_wallpaper/treelandwallpapermanagerclient.h index cbaeb406f..ec9a45c96 100644 --- a/examples/test_set_wallpaper/treelandwallpapermanagerclient.h +++ b/examples/test_set_wallpaper/treelandwallpapermanagerclient.h @@ -15,7 +15,7 @@ class TreelandWallpaperManagerV1 { Q_OBJECT public: - static constexpr int InterfaceVersion = 1; + static constexpr int InterfaceVersion = 2; explicit TreelandWallpaperManagerV1(); ~TreelandWallpaperManagerV1() override; diff --git a/src/modules/wallpaper/wallpapermanagerinterfacev1.cpp b/src/modules/wallpaper/wallpapermanagerinterfacev1.cpp index 26143e832..2224569be 100644 --- a/src/modules/wallpaper/wallpapermanagerinterfacev1.cpp +++ b/src/modules/wallpaper/wallpapermanagerinterfacev1.cpp @@ -163,6 +163,7 @@ class TreelandWallpaperInterfaceV1Private : public QtWaylandServer::treeland_wal void destroy(Resource *resource) override; void set_image_source(Resource *resource, const QString &fileSource, uint32_t nativeRole) override; void set_video_source(Resource *resource, const QString &fileSource, uint32_t nativeRole) override; + void set_shader_source(Resource *resource, const QString &fileSource, uint32_t nativeRole) override; }; TreelandWallpaperInterfaceV1Private::TreelandWallpaperInterfaceV1Private(TreelandWallpaperInterfaceV1 *_q, @@ -211,6 +212,17 @@ void TreelandWallpaperInterfaceV1Private::set_video_source([[maybe_unused]] Reso Q_EMIT q->videoSourceChanged(workspace->currentIndex(), fileSource, roles); } +void TreelandWallpaperInterfaceV1Private::set_shader_source([[maybe_unused]] Resource *resource, + const QString &fileSource, + uint32_t nativeRole) +{ + TreelandWallpaperInterfaceV1::WallpaperRoles roles = + static_cast(nativeRole); + Workspace *workspace = Helper::instance()->workspace(); + Q_ASSERT(workspace); + Q_EMIT q->shaderSourceChanged(workspace->currentIndex(), fileSource, roles); +} + TreelandWallpaperInterfaceV1::TreelandWallpaperInterfaceV1(WOutput *output, const QString &userName, wl_resource *resource, @@ -249,6 +261,10 @@ void TreelandWallpaperInterfaceV1::sendError(const QString &source, Error error) void TreelandWallpaperInterfaceV1::sendChanged(WallpaperRoles roles, WallpaperType type, const QString &source) { + if (type == Shader && wl_resource_get_version(d->resource) < TreelandWallpaperManagerInterfaceV1::InterfaceVersion) { + return; + } + d->send_changed(roles, type, source); } diff --git a/src/modules/wallpaper/wallpapermanagerinterfacev1.h b/src/modules/wallpaper/wallpapermanagerinterfacev1.h index b10f22e23..e512b7698 100644 --- a/src/modules/wallpaper/wallpapermanagerinterfacev1.h +++ b/src/modules/wallpaper/wallpapermanagerinterfacev1.h @@ -24,7 +24,7 @@ class TreelandWallpaperManagerInterfaceV1 : public QObject , public WServerInter QByteArrayView interfaceName() const override; - static constexpr int InterfaceVersion = 1; + static constexpr int InterfaceVersion = 2; Q_SIGNALS: void wallpaperCreated(TreelandWallpaperInterfaceV1 *interface); @@ -52,6 +52,7 @@ class TreelandWallpaperInterfaceV1 : public QObject enum WallpaperType { Image = 0, Video = 1, + Shader = 2, }; enum WallpaperRole { @@ -79,6 +80,9 @@ class TreelandWallpaperInterfaceV1 : public QObject void videoSourceChanged(int workspaceIndex, const QString &fileSource, WallpaperRoles roles); + void shaderSourceChanged(int workspaceIndex, + const QString &fileSource, + WallpaperRoles roles); private: explicit TreelandWallpaperInterfaceV1(WOutput *output, diff --git a/src/modules/wallpaper/wallpapernotifierinterfacev1.cpp b/src/modules/wallpaper/wallpapernotifierinterfacev1.cpp index 18df6fbf9..01dae42a3 100644 --- a/src/modules/wallpaper/wallpapernotifierinterfacev1.cpp +++ b/src/modules/wallpaper/wallpapernotifierinterfacev1.cpp @@ -49,6 +49,11 @@ TreelandWallpaperNotifierInterfaceV1::TreelandWallpaperNotifierInterfaceV1(QObje void TreelandWallpaperNotifierInterfaceV1::sendAdd(TreelandWallpaperInterfaceV1::WallpaperType type, const QString &fileSource) { for (const auto &resource : d->resourceMap()) { + if (type == TreelandWallpaperInterfaceV1::Shader + && resource->version() < TreelandWallpaperNotifierInterfaceV1::InterfaceVersion) { + continue; + } + d->send_add(resource->handle, type, fileSource); } } @@ -84,5 +89,10 @@ QByteArrayView TreelandWallpaperNotifierInterfaceV1::interfaceName() const void TreelandWallpaperNotifierInterfaceV1::sendAddForResource(wl_resource *resource, TreelandWallpaperInterfaceV1::WallpaperType type, const QString &fileSource) { + if (type == TreelandWallpaperInterfaceV1::Shader + && wl_resource_get_version(resource) < TreelandWallpaperNotifierInterfaceV1::InterfaceVersion) { + return; + } + d->send_add(resource, type, fileSource); } diff --git a/src/modules/wallpaper/wallpapernotifierinterfacev1.h b/src/modules/wallpaper/wallpapernotifierinterfacev1.h index e0d46c218..5ed4268ef 100644 --- a/src/modules/wallpaper/wallpapernotifierinterfacev1.h +++ b/src/modules/wallpaper/wallpapernotifierinterfacev1.h @@ -25,7 +25,7 @@ class TreelandWallpaperNotifierInterfaceV1 : public QObject , public WServerInte QByteArrayView interfaceName() const override; - static constexpr int InterfaceVersion = 1; + static constexpr int InterfaceVersion = 2; void sendAddForResource(wl_resource *resource, TreelandWallpaperInterfaceV1::WallpaperType type, const QString &fileSource); void sendAdd(TreelandWallpaperInterfaceV1::WallpaperType type, const QString &fileSource); diff --git a/src/wallpaper/wallpaperconfig.cpp b/src/wallpaper/wallpaperconfig.cpp index 25b2a7ecd..7a9ee02c0 100644 --- a/src/wallpaper/wallpaperconfig.cpp +++ b/src/wallpaper/wallpaperconfig.cpp @@ -3,6 +3,8 @@ #include "wallpaperconfig.h" +#include "common/treelandlogging.h" + #define OUTPUT_NAME "outputName" #define LOCK_SCREEN_WALLAPER_TYPE "lockScreenWallpaperType" #define DESKTOP_WALLAPER_TYPE "desktopWallpaperType" @@ -12,6 +14,21 @@ #define WORKSPACES "workspaces" #define ENABLE "enable" +static TreelandWallpaperInterfaceV1::WallpaperType wallpaperTypeFromJson(const QJsonObject &obj, const char *key) +{ + const auto type = static_cast(obj[key].toInt()); + switch (type) { + case TreelandWallpaperInterfaceV1::Image: + case TreelandWallpaperInterfaceV1::Video: + case TreelandWallpaperInterfaceV1::Shader: + return type; + } + + qCWarning(lcTlWallpaper) << "Unsupported wallpaper type in config:" << obj[key].toInt() + << "key:" << key << "- falling back to image"; + return TreelandWallpaperInterfaceV1::Image; +} + QJsonObject WallpaperWorkspaceConfig::toJson() const { QJsonObject obj; @@ -27,7 +44,7 @@ WallpaperWorkspaceConfig WallpaperWorkspaceConfig::fromJson(const QJsonObject &o WallpaperWorkspaceConfig ws; ws.workspaceId = obj[WORKSPACE_INDEX].toInt(); ws.desktopWallpaper = obj[DESKTOP_WALLPAPER].toString(); - ws.desktopWallpapertype = static_cast(obj[DESKTOP_WALLAPER_TYPE].toInt()); + ws.desktopWallpapertype = wallpaperTypeFromJson(obj, DESKTOP_WALLAPER_TYPE); ws.enable = obj[ENABLE].toBool(); return ws; } @@ -64,7 +81,7 @@ WallpaperOutputConfig WallpaperOutputConfig::fromJson(const QJsonObject &obj) WallpaperOutputConfig out; out.outputName = obj[OUTPUT_NAME].toString(); out.lockscreenWallpaper = obj[LOCK_SCREEN_WALLPAPER].toString(); - out.lockScreenWallpapertype = static_cast(obj[LOCK_SCREEN_WALLAPER_TYPE].toInt()); + out.lockScreenWallpapertype = wallpaperTypeFromJson(obj, LOCK_SCREEN_WALLAPER_TYPE); out.enable = obj[ENABLE].toBool(); QJsonArray workspacesArray = obj[WORKSPACES].toArray(); for (const QJsonValue& value : std::as_const(workspacesArray)) { diff --git a/src/wallpaper/wallpapermanager.cpp b/src/wallpaper/wallpapermanager.cpp index 3d29216da..252d338a5 100644 --- a/src/wallpaper/wallpapermanager.cpp +++ b/src/wallpaper/wallpapermanager.cpp @@ -17,9 +17,22 @@ enum class WallpaperType { Image, Video, + Shader, Unknown }; +static bool isValidWallpaperType(TreelandWallpaperInterfaceV1::WallpaperType type) +{ + switch (type) { + case TreelandWallpaperInterfaceV1::Image: + case TreelandWallpaperInterfaceV1::Video: + case TreelandWallpaperInterfaceV1::Shader: + return true; + } + + return false; +} + static WallpaperType detectWallpaperType(const QString &path) { if (path.isEmpty()) @@ -281,6 +294,12 @@ QString WallpaperManager::wallpaperConfigToJsonString() void WallpaperManager::setOutputWallpaper(wlr_output *output, [[maybe_unused]] int workspaceIndex, const QString &fileSource, TreelandWallpaperInterfaceV1::WallpaperRoles roles, TreelandWallpaperInterfaceV1::WallpaperType type) { + if (!isValidWallpaperType(type)) { + qCWarning(lcTlWallpaper) << "Rejecting unsupported wallpaper type:" << type + << "source:" << fileSource; + return; + } + bool update = false; for (WallpaperOutputConfig &outputConfig : m_wallpaperConfig) { if (outputConfig.outputName == getOutputId(output)) { @@ -479,6 +498,10 @@ void WallpaperManager::onWallpaperAdded(TreelandWallpaperInterfaceV1 *interface) &TreelandWallpaperInterfaceV1::videoSourceChanged, this, &WallpaperManager::onVideoChanged); + connect(interface, + &TreelandWallpaperInterfaceV1::shaderSourceChanged, + this, + &WallpaperManager::onShaderChanged); } void WallpaperManager::onImageChanged(int workspaceIndex, const QString &fileSource, TreelandWallpaperInterfaceV1::WallpaperRoles roles) @@ -525,6 +548,28 @@ void WallpaperManager::onVideoChanged(int workspaceIndex, const QString &fileSou } } +void WallpaperManager::onShaderChanged(int workspaceIndex, const QString &fileSource, TreelandWallpaperInterfaceV1::WallpaperRoles roles) +{ + TreelandWallpaperInterfaceV1 *interface = + static_cast(sender()); + auto *output = interface->wOutput(); + if (!output) + return; + + QMap globalWallpapers = globalValidWallpaper(nullptr, -1); + setOutputWallpaper(output->nativeHandle(), + workspaceIndex, + fileSource, + roles, + TreelandWallpaperInterfaceV1::Shader); + if (globalWallpapers.contains(fileSource)) { + interface->sendError(fileSource, TreelandWallpaperInterfaceV1::AlreadyUsed); + Q_EMIT updateWallpaper(); + } else { + Helper::instance()->m_wallpaperNotifierInterfaceV1->sendAdd(TreelandWallpaperInterfaceV1::Shader, fileSource); + } +} + void WallpaperManager::onWallpaperNotifierBound(wl_resource *resource) { QMap globalWallpapers = globalValidWallpaper(nullptr, -1); diff --git a/src/wallpaper/wallpapermanager.h b/src/wallpaper/wallpapermanager.h index 67c4e9510..4e8189d48 100644 --- a/src/wallpaper/wallpapermanager.h +++ b/src/wallpaper/wallpapermanager.h @@ -56,6 +56,9 @@ public Q_SLOTS: void onVideoChanged(int workspaceIndex, const QString &fileSource, TreelandWallpaperInterfaceV1::WallpaperRoles roles); + void onShaderChanged(int workspaceIndex, + const QString &fileSource, + TreelandWallpaperInterfaceV1::WallpaperRoles roles); void onWallpaperNotifierBound(wl_resource *resource); void handleWallpaperSurfaceAdded(TreelandWallpaperSurfaceInterfaceV1 *interface); diff --git a/wallpaper-factory/CMakeLists.txt b/wallpaper-factory/CMakeLists.txt index 2cac36c17..a12fa52f4 100644 --- a/wallpaper-factory/CMakeLists.txt +++ b/wallpaper-factory/CMakeLists.txt @@ -33,12 +33,15 @@ qt_add_qml_module(${BIN_NAME} qwaylandwallpapershellintegrationplugin.cpp qwaylandwallpapersurface_p.h qwaylandwallpapersurface.cpp + shaderframeclock.h + shaderframeclock.cpp wallpaperwindow.h wallpaperwindow.cpp treelandwallpapernotifierclient.h treelandwallpapernotifierclient.cpp QML_FILES Image.qml + Shader.qml Video.qml ) diff --git a/wallpaper-factory/Shader.qml b/wallpaper-factory/Shader.qml new file mode 100644 index 000000000..09a85d866 --- /dev/null +++ b/wallpaper-factory/Shader.qml @@ -0,0 +1,81 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick + +Item { + id: root + + property alias fragmentShader: shader.fragmentShader + property bool running: true + property alias playbackRate: clock.playbackRate + property alias iTime: clock.time + property vector2d iResolution: Qt.vector2d(width, height) + property vector4d iMouse: Qt.vector4d(0, 0, 0, 0) + + function startUp() { + slowDownAnimation.stop() + startUpAnimation.stop() + running = true + playbackRate = 0.0 + startUpAnimation.start() + } + + function slowDown(duration) { + startUpAnimation.stop() + slowDownAnimation.stop() + playbackRate = 1.0 + running = true + slowDownAnimation.duration = duration + slowDownAnimation.start() + } + + onRunningChanged: { + if (running && !slowDownAnimation.running) { + startUp() + } + } + + Component.onCompleted: startUp() + + ShaderFrameClock { + id: clock + + running: root.running && root.visible + playbackRate: 0.0 + } + + Rectangle { + anchors.fill: parent + color: "black" + } + + ShaderEffect { + id: shader + + anchors.fill: parent + property real iTime: root.iTime + property vector2d iResolution: root.iResolution + property vector4d iMouse: root.iMouse + } + + NumberAnimation { + id: slowDownAnimation + + target: clock + property: "playbackRate" + to: 0.0 + easing.type: Easing.OutExpo + onFinished: root.running = false + } + + NumberAnimation { + id: startUpAnimation + + target: clock + property: "playbackRate" + to: 1.0 + duration: 650 + easing.type: Easing.OutCubic + } +} diff --git a/wallpaper-factory/shaderframeclock.cpp b/wallpaper-factory/shaderframeclock.cpp new file mode 100644 index 000000000..f9507ea1f --- /dev/null +++ b/wallpaper-factory/shaderframeclock.cpp @@ -0,0 +1,115 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "shaderframeclock.h" + +#include + +ShaderFrameClock::ShaderFrameClock(QQuickItem *parent) + : QQuickItem(parent) +{ +} + +bool ShaderFrameClock::running() const +{ + return m_running; +} + +void ShaderFrameClock::setRunning(bool running) +{ + if (m_running == running) + return; + + m_running = running; + m_elapsedTimer.restart(); + Q_EMIT runningChanged(); + requestNextFrame(); +} + +qreal ShaderFrameClock::playbackRate() const +{ + return m_playbackRate; +} + +void ShaderFrameClock::setPlaybackRate(qreal playbackRate) +{ + if (qFuzzyCompare(m_playbackRate, playbackRate)) + return; + + m_playbackRate = playbackRate; + Q_EMIT playbackRateChanged(); + requestNextFrame(); +} + +qreal ShaderFrameClock::time() const +{ + return m_time; +} + +void ShaderFrameClock::setTime(qreal time) +{ + if (qFuzzyCompare(m_time, time)) + return; + + m_time = time; + Q_EMIT timeChanged(); + requestNextFrame(); +} + +void ShaderFrameClock::itemChange(ItemChange change, const ItemChangeData &data) +{ + QQuickItem::itemChange(change, data); + + if (change == ItemSceneChange) + connectWindow(data.window); +} + +void ShaderFrameClock::connectWindow(QQuickWindow *window) +{ + if (m_window == window) + return; + + disconnectWindow(); + m_window = window; + m_elapsedTimer.restart(); + + if (!m_window) + return; + + connect(m_window, &QQuickWindow::frameSwapped, + this, &ShaderFrameClock::tick); + requestNextFrame(); +} + +void ShaderFrameClock::disconnectWindow() +{ + if (!m_window) + return; + + disconnect(m_window, nullptr, this, nullptr); + m_window = nullptr; +} + +void ShaderFrameClock::tick() +{ + if (!m_elapsedTimer.isValid()) + m_elapsedTimer.restart(); + + const qint64 elapsed = m_elapsedTimer.nsecsElapsed(); + m_elapsedTimer.restart(); + + if (m_running && m_playbackRate > 0.0) { + m_time += (elapsed / 1000000000.0) * m_playbackRate; + Q_EMIT timeChanged(); + } + + requestNextFrame(); +} + +void ShaderFrameClock::requestNextFrame() +{ + if (!m_window || !m_running) + return; + + m_window->requestUpdate(); +} diff --git a/wallpaper-factory/shaderframeclock.h b/wallpaper-factory/shaderframeclock.h new file mode 100644 index 000000000..46b6782fc --- /dev/null +++ b/wallpaper-factory/shaderframeclock.h @@ -0,0 +1,52 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include +#include +#include + +class QQuickWindow; + +class ShaderFrameClock : public QQuickItem +{ + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(bool running READ running WRITE setRunning NOTIFY runningChanged) + Q_PROPERTY(qreal playbackRate READ playbackRate WRITE setPlaybackRate NOTIFY playbackRateChanged) + Q_PROPERTY(qreal time READ time WRITE setTime NOTIFY timeChanged) + +public: + explicit ShaderFrameClock(QQuickItem *parent = nullptr); + + bool running() const; + void setRunning(bool running); + + qreal playbackRate() const; + void setPlaybackRate(qreal playbackRate); + + qreal time() const; + void setTime(qreal time); + +Q_SIGNALS: + void runningChanged(); + void playbackRateChanged(); + void timeChanged(); + +protected: + void itemChange(ItemChange change, const ItemChangeData &data) override; + +private: + void connectWindow(QQuickWindow *window); + void disconnectWindow(); + void tick(); + void requestNextFrame(); + + QQuickWindow *m_window = nullptr; + QElapsedTimer m_elapsedTimer; + bool m_running = true; + qreal m_playbackRate = 1.0; + qreal m_time = 0.0; +}; diff --git a/wallpaper-factory/treelandwallpapernotifierclient.cpp b/wallpaper-factory/treelandwallpapernotifierclient.cpp index 7f7223981..dedbc2f9c 100644 --- a/wallpaper-factory/treelandwallpapernotifierclient.cpp +++ b/wallpaper-factory/treelandwallpapernotifierclient.cpp @@ -8,9 +8,20 @@ #include -#define TREELANDWALLPAPERPRODUCEV1VERSION 1 +#include +#include +#include +#include +#include + +#define TREELANDWALLPAPERPRODUCEV1VERSION 2 #define WALLPAPERSOURCE "wallpaperSource" +static QString shaderFragmentPath(const QString &packagePath) +{ + return QDir(packagePath).filePath(QStringLiteral("fragment.qsb")); +} + static QSize maxScreenSize() { QSize maxSize; @@ -123,6 +134,40 @@ void TreelandWallpaperNotifierClientV1::treeland_wallpaper_notifier_v1_add(uint3 break; } + case QtWayland::treeland_wallpaper_notifier_v1:: + wallpaper_source_type::wallpaper_source_type_shader: { + const QString fragmentPath = shaderFragmentPath(file_source); + const QFileInfo packageInfo(file_source); + const QFileInfo fragmentInfo(fragmentPath); + if (!packageInfo.isDir() || !fragmentInfo.isFile() || !fragmentInfo.isReadable()) { + qCWarning(WALLPAPER) << "Shader wallpaper package is not readable" + << "package:" << file_source + << "packageExists:" << packageInfo.exists() + << "packageIsDir:" << packageInfo.isDir() + << "fragment:" << fragmentPath + << "fragmentExists:" << fragmentInfo.exists() + << "fragmentIsFile:" << fragmentInfo.isFile() + << "fragmentReadable:" << fragmentInfo.isReadable(); + delete wallpaperWindow; + return; + } + + wallpaperWindow->loadFromModule("com.treeland.wallfactory", "Shader"); + wallpaperWindow->setColor(QColor(Qt::black)); + QObject *root = wallpaperWindow->rootObject(); + if (!root) { + qCWarning(WALLPAPER) << "Failed to load Shader wallpaper QML" + << "package:" << file_source + << "fragment:" << fragmentPath; + delete wallpaperWindow; + return; + } + + root->setProperty("fragmentShader", QUrl::fromLocalFile(fragmentPath)); + window->setLoaded(true); + break; + } + default: qCCritical(WALLPAPER) << "Unsupported wallpaper source type:" << source_type; @@ -162,11 +207,8 @@ void TreelandWallpaperNotifierClientV1::updateAllRefreshInterval() foreach(auto window, std::as_const(m_windows)) { QObject *root = window->rootObject(); auto *video = qobject_cast(root); - if (!video) { - continue; - } - - video->setRefreshInterval(minRefreshInterval); + if (video) + video->setRefreshInterval(minRefreshInterval); } } @@ -210,6 +252,8 @@ void TreelandWallpaperNotifierClientV1::onPlayChanged(bool play) auto *video = qobject_cast(root); if (video) { video->setPause(!play); + } else if (root->property("running").isValid()) { + root->setProperty("running", play); } else { if (QQuickAnimatedImage *image = qobject_cast(root)) { if (image->frameCount() > 1) { @@ -229,6 +273,8 @@ void TreelandWallpaperNotifierClientV1::onSlowDownChanged(uint32_t duration) auto *video = qobject_cast(root); if (video) { video->slowDown(duration); + } else if (root->property("running").isValid()) { + QMetaObject::invokeMethod(root, "slowDown", Q_ARG(QVariant, QVariant::fromValue(duration))); } else { if (QQuickAnimatedImage *image = qobject_cast(root)) { if (image->frameCount() > 1) {