Files
z-bar-qt/Plugins/ZShell/Internal/wallpaperservice.cpp
T
zach e655ec5bc7
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
feat: add wallpaper transitions, store crop in database per-wallpaper, move CUDA context creation to async thread
2026-10-26 14:47:47 +01:00

2757 lines
68 KiB
C++

#define EGL_EGLEXT_PROTOTYPES
#define GL_GLEXT_PROTOTYPES
#include "wallpaperservice.hpp"
#include <EGL/eglext.h>
#include <GL/gl.h>
#include <GL/glext.h>
#ifndef GL_TEXTURE_EXTERNAL_OES
#define GL_TEXTURE_EXTERNAL_OES 0x8D66
#endif
#include <mpv/client.h>
#include <mpv/render_gl.h>
#include <clocale>
#include <fcntl.h>
#include <poll.h>
#include <unistd.h>
#include <QDebug>
#include <QElapsedTimer>
#include <QFileInfo>
#include <QGuiApplication>
#include <QImageReader>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRandomGenerator>
#include <QScreen>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QThread>
#include <QThreadPool>
#include <QCryptographicHash>
#include <QDir>
#include <QFile>
#include <QProcess>
#include <qguiapplication_platform.h>
#include <private/qwaylandscreen_p.h>
#include "config/background.hpp"
#include "config/rootnodes.hpp"
#include <cstring>
namespace ZShell::internal {
Q_LOGGING_CATEGORY(wpLog, "ZShell.Internal.wallpaper")
namespace {
#ifdef ZHAVE_VIDEO
const bool kMpvMode = qEnvironmentVariableIsSet("ZBAR_WALLPAPER_MPV");
#endif
#ifdef ZHAVE_VIDEO
const bool kGlesMode = qEnvironmentVariableIsSet("ZBAR_WALLPAPER_GLES") ||
kMpvMode;
#else
const bool kGlesMode = qEnvironmentVariableIsSet("ZBAR_WALLPAPER_GLES");
#endif
const char* shaderHeader(bool fragment) {
if (kGlesMode)
return fragment ? "#version 300 es\nprecision highp float;\n"
: "#version 300 es\n";
return "#version 330\n";
}
QByteArray shaderSource(const QString& file, bool fragment) {
QFile source(QStringLiteral(":/shaders/") + file);
if (!source.open(QIODevice::ReadOnly)) {
qWarning(wpLog) << "WallpaperService: could not read the shader"
<< file;
return {};
}
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;
const QStringList kVideoExtensions = {
"mp4",
"mkv",
"webm",
"avi",
"mov",
"wmv",
"flv",
"m4v",
"mpg",
"mpeg",
"3gp",
"ogv",
"ts",
"vob",
};
void logShader(uint32_t object, bool program) {
int ok = 0;
if (program)
glGetProgramiv(object, GL_LINK_STATUS, &ok);
else
glGetShaderiv(object, GL_COMPILE_STATUS, &ok);
if (ok) return;
char info[512] = {};
if (program)
glGetProgramInfoLog(object, int(sizeof(info)), nullptr, info);
else
glGetShaderInfoLog(object, int(sizeof(info)), nullptr, info);
qWarning(wpLog) << "WallpaperService:"
<< (program ? "linking" : "compiling") << "failed:" << info;
}
QVariantMap cropFor(const QVariantMap& crops, const QString& screen) {
QVariantMap crop = crops.value(screen).toMap();
if (crop.isEmpty()) {
crop.insert("x", 0.0);
crop.insert("y", 0.0);
crop.insert("width", 1.0);
crop.insert("height", 1.0);
crop.insert("zoom", 1.0);
}
return crop;
}
} // namespace
WallpaperService::WallpaperService(QObject* parent) : QObject(parent) {
auto* background =
ZShell::config::ConfigSingleton::instance()->background();
{
std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> 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;
wake();
if (m_thread.joinable()) m_thread.join();
if (m_mpvRender) {
mpv_render_context_free(m_mpvRender);
m_mpvRender = nullptr;
}
if (m_mpv) {
mpv_terminate_destroy(m_mpv);
m_mpv = nullptr;
}
for (auto* surface : m_surfaces)
delete surface;
m_surfaces.clear();
if (m_eglDisplay != EGL_NO_DISPLAY && m_context != EGL_NO_CONTEXT) {
if (eglMakeCurrent(
m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, m_context)) {
if (m_texture) glDeleteTextures(1, &m_texture);
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();
m_player->deleteTexture();
m_player.reset();
}
#endif
}
eglDestroyContext(m_eglDisplay, m_context);
}
if (m_queue) wl_event_queue_destroy(m_queue);
if (m_wake[0] >= 0) {
close(m_wake[0]);
close(m_wake[1]);
}
if (m_layerShell) m_layerShell->destroy();
if (m_viewporter) m_viewporter->destroy();
if (m_scaleManager) m_scaleManager->destroy();
if (m_registry) wl_registry_destroy(m_registry);
}
QString WallpaperService::path() const {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
return m_path;
}
QString WallpaperService::dbPath() const {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
return m_dbPath;
}
void WallpaperService::setDbPath(const QString& path) {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (m_dbPath == path) return;
m_dbPath = path;
m_forceDecode = true;
}
emit dbPathChanged();
wake();
}
void WallpaperService::setPath(const QString& path) {
if (path.isEmpty() || path == this->path()) return;
if (isVideoPath(path) && needsTranscode(path)) {
const QString dst = transcodeTarget(path);
if (QFile::exists(dst)) {
applyPath(dst);
return;
}
applyPath(path);
transcodeVideo(path, dst);
return;
}
applyPath(path);
}
void WallpaperService::applyPath(const QString& path) {
const bool video = isVideoPath(path);
{
std::lock_guard<std::recursive_mutex> 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
const bool videoBackend = m_mpv != nullptr;
#endif
const bool wasVideo = m_isVideo && videoBackend;
const bool wasImage = !m_image.isNull() && m_texture != 0;
const bool beginBurst = m_transitions.empty();
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_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<int>(m_transitions.size()) > kMaxTransitions)
m_promoteOldest = true;
while (static_cast<int>(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();
start();
queueAll();
}
bool WallpaperService::isHardwareCodec(const QString& codec) {
static const QStringList hardware{
"av1",
"hevc",
"h264",
"vp9",
"vp8",
"vc1",
"mpeg2",
"mpeg2video",
"mpeg4",
"mjpeg",
};
return hardware.contains(codec);
}
QString WallpaperService::codecName(const QString& path) const {
QProcess probe;
probe.start(
"ffprobe",
{
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=codec_name",
"-of",
"default=nw=1:nk=1",
path,
});
if (!probe.waitForFinished(2000) || probe.exitCode() != 0) {
qWarning(wpLog) << "WallpaperService: ffprobe failed for" << path
<< "(exit" << probe.exitCode() << ")";
return {};
}
const QString codec =
QString::fromUtf8(probe.readAllStandardOutput()).trimmed().toLower();
return codec;
}
QString WallpaperService::cacheDir() const {
return QDir::homePath() + "/.cache/zshell/wallpaper-videos";
}
QString WallpaperService::transcodeTarget(const QString& path) const {
const QFileInfo info(path);
const QByteArray key =
path.toUtf8() + '|' + QByteArray::number(info.size()) + '|' +
QByteArray::number(info.lastModified().toMSecsSinceEpoch());
const QString hash = QString::fromUtf8(
QCryptographicHash::hash(key, QCryptographicHash::Sha1).toHex());
return cacheDir() + '/' + hash + ".av1.mkv";
}
bool WallpaperService::needsTranscode(const QString& path) const {
if (path.startsWith(cacheDir())) return false;
const QString codec = codecName(path);
if (codec.isEmpty()) return false;
return !isHardwareCodec(codec);
}
void WallpaperService::transcodeVideo(const QString& src, const QString& dst) {
QDir().mkpath(cacheDir());
if (m_transcode) {
m_transcode->kill();
m_transcode->deleteLater();
m_transcode = nullptr;
}
m_transcode = new QProcess(this);
auto* proc = m_transcode;
connect(proc, &QProcess::readyReadStandardError, proc, [proc]() {
qWarning(wpLog).noquote()
<< "ffmpeg:" << QString::fromUtf8(proc->readAllStandardError());
});
connect(
proc,
&QProcess::finished,
this,
[this, proc, dst](int code, QProcess::ExitStatus status) {
const bool ok = code == 0 && status == QProcess::NormalExit &&
QFile::exists(dst);
qWarning(wpLog) << "WallpaperService: transcode finished, exit"
<< code << "status" << int(status) << "exists"
<< QFile::exists(dst) << "->" << dst;
if (proc == m_transcode) m_transcode = nullptr;
proc->deleteLater();
if (ok) applyPath(dst);
});
qWarning(wpLog) << "WallpaperService: transcoding" << src << "->" << dst;
proc->setProgram("nice");
proc->setArguments({
"-n",
"19",
"ffmpeg",
"-hide_banner",
"-loglevel",
"error",
"-y",
"-threads",
"2",
"-i",
src,
"-map",
"0:v:0",
"-vf",
"scale=in_range=pc:out_range=tv:out_color_matrix=bt709",
"-colorspace",
"bt709",
"-color_primaries",
"bt709",
"-color_trc",
"bt709",
"-color_range",
"tv",
"-c:v",
"libsvtav1",
"-preset",
"6",
"-crf",
"20",
"-pix_fmt",
"yuv420p10le",
"-an",
dst,
});
proc->start();
}
void WallpaperService::addScreen(const QString& screen) {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (m_screens.contains(screen)) return;
m_screens.append(screen);
}
start();
queueAll();
}
void WallpaperService::removeScreen(const QString& screen) {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (!m_screens.removeOne(screen)) return;
}
queueAll();
}
const QVariantMap& WallpaperService::crops() const {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
return m_crops;
}
void WallpaperService::setCrops(const QVariantMap& crops) {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (m_crops == crops) return;
m_crops = crops;
}
emit cropsChanged();
queueAll();
}
void WallpaperService::setCrop(const QString& screen, const QVariantMap& crop) {
WallpaperSurface* surface = nullptr;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (m_crops.value(screen) == crop) return;
m_crops.insert(screen, crop);
for (auto* candidate : m_surfaces) {
if (candidate->screen() == screen) {
surface = candidate;
break;
}
}
}
emit cropsChanged();
if (surface) queue(surface);
}
void WallpaperService::redraw() {
queueAll();
}
bool WallpaperService::isVideoPath(const QString& path) {
return kVideoExtensions.contains(QFileInfo(path).suffix().toLower());
}
void WallpaperService::startVideo(const QString& path) {
if (path.isEmpty()) return;
m_videoReady.store(false, std::memory_order_relaxed);
m_videoLoaded.store(false, std::memory_order_relaxed);
#ifdef ZHAVE_VIDEO
if (m_usePlayer && !kMpvMode) {
if (startPlayerVideo(path)) return;
qWarning(wpLog) << "WallpaperService: the GPU video player failed, "
"falling back to mpv";
m_usePlayer = false;
m_player.reset();
}
#endif
if (!m_mpv) {
std::setlocale(LC_NUMERIC, "C");
m_mpv = mpv_create();
if (!m_mpv) {
qWarning(wpLog) << "WallpaperService: could not create mpv handle";
return;
}
mpv_set_option_string(m_mpv, "config", "no");
mpv_set_option_string(m_mpv, "vo", "libmpv");
mpv_set_option_string(m_mpv, "audio", "no");
mpv_set_option_string(m_mpv, "loop", "inf");
mpv_set_option_string(m_mpv, "hwdec", "auto-copy");
mpv_set_option_string(m_mpv, "video-sync", "display");
mpv_set_option_string(m_mpv, "framedrop", "decoder");
mpv_set_option_string(m_mpv, "background-color", "#00000000");
if (mpv_initialize(m_mpv) < 0) {
qWarning(wpLog) << "WallpaperService: mpv_initialize failed";
mpv_terminate_destroy(m_mpv);
m_mpv = nullptr;
return;
}
}
const QByteArray local = path.toUtf8();
initMpvRender();
const char* loadCmd[]{"loadfile", local.constData(), nullptr};
if (mpv_command(m_mpv, loadCmd) < 0)
qWarning(wpLog) << "WallpaperService: mpv could not load" << path;
m_mpvFrameReady.store(true, std::memory_order_relaxed);
}
void WallpaperService::pumpMpvEvents() {
if (!m_mpv) return;
mpv_event* event = nullptr;
while ((event = mpv_wait_event(m_mpv, 0)) != nullptr &&
event->event_id != MPV_EVENT_NONE) {
if (event->event_id == MPV_EVENT_FILE_LOADED) {
m_videoLoaded.store(true, std::memory_order_relaxed);
}
if (event->event_id == MPV_EVENT_END_FILE) {
m_videoLoaded.store(false, std::memory_order_relaxed);
m_videoReady.store(false, std::memory_order_relaxed);
auto* data = static_cast<mpv_event_end_file*>(event->data);
if (data && data->error != MPV_END_FILE_REASON_EOF)
qWarning(wpLog) << "WallpaperService: mpv ended the file, error"
<< data->error;
}
}
}
void WallpaperService::teardownMpv() {
if (m_mpvRender) {
mpv_render_context_free(m_mpvRender);
m_mpvRender = nullptr;
}
if (m_mpv) {
mpv_terminate_destroy(m_mpv);
m_mpv = nullptr;
}
m_mpvFrameReady.store(false, std::memory_order_relaxed);
m_videoReady.store(false, std::memory_order_relaxed);
m_videoLoaded.store(false, std::memory_order_relaxed);
}
void* WallpaperService::getMpvProcAddress(void* ctx, const char* name) {
(void)ctx;
if (strcmp(name, "glFenceSync") == 0)
return reinterpret_cast<void*>(
+[](GLenum, GLbitfield) -> GLsync { return nullptr; });
return reinterpret_cast<void*>(eglGetProcAddress(name));
}
void WallpaperService::mpvUpdate(void* ctx) {
auto* self = static_cast<WallpaperService*>(ctx);
self->m_mpvFrameReady.store(true, std::memory_order_relaxed);
if (self->m_videoLoaded.load(std::memory_order_relaxed))
self->m_videoReady.store(true, std::memory_order_relaxed);
self->wake();
}
void WallpaperService::initMpvRender() {
if (m_mpvRender || !m_mpv) return;
mpv_opengl_init_params gl_init{getMpvProcAddress, nullptr};
mpv_render_param params[]{
{MPV_RENDER_PARAM_API_TYPE,
const_cast<char*>(MPV_RENDER_API_TYPE_OPENGL)},
{MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init},
{MPV_RENDER_PARAM_INVALID, nullptr},
};
const int rc = mpv_render_context_create(&m_mpvRender, m_mpv, params);
if (rc < 0) {
qWarning(wpLog)
<< "WallpaperService: could not create mpv render context, code"
<< rc;
m_mpvRender = nullptr;
return;
}
mpv_render_context_set_update_callback(m_mpvRender, mpvUpdate, this);
}
void WallpaperService::renderMpvToSurface(const QSize& size) {
if (!m_mpvRender) initMpvRender();
if (!m_mpvRender) return;
glViewport(0, 0, size.width(), size.height());
mpv_opengl_fbo fbo{0, 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);
}
void WallpaperService::createBlackTexture(uint32_t& texture) {
if (texture != 0) return;
const unsigned char pixel[4] = {0, 0, 0, 255};
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(
GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
}
void WallpaperService::start() {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (started || m_path.isEmpty() || m_screens.isEmpty()) return;
started = true;
}
auto* interface =
qApp->nativeInterface<QNativeInterface::QWaylandApplication>();
if (!interface) {
qWarning(wpLog) << "WallpaperService: this is not a Wayland session";
return;
}
m_display = interface->display();
m_compositor = interface->compositor();
if (!m_display || !m_compositor) return;
m_queue = wl_display_create_queue(m_display);
if (!m_queue) return;
if (pipe2(m_wake, O_CLOEXEC) < 0) {
qWarning(wpLog)
<< "WallpaperService: could not make the pipe to wake on";
return;
}
bindGlobals();
if (!setupGl()) return;
m_drawing = true;
m_thread = std::thread([this] { loop(); });
wake();
}
void WallpaperService::wake() {
if (m_wake[1] < 0) return;
char byte = 0;
if (::write(m_wake[1], &byte, 1) < 0)
qWarning(wpLog)
<< "WallpaperService: could not wake the drawing thread";
}
void WallpaperService::queue(WallpaperSurface* surface) {
surface->markDirty();
wake();
}
void WallpaperService::queueAll() {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
for (auto* surface : m_surfaces)
surface->markDirty();
}
wake();
}
void WallpaperService::loop() {
eglBindAPI(kGlesMode ? EGL_OPENGL_ES_API : EGL_OPENGL_API);
if (m_context == EGL_NO_CONTEXT) {
if (kGlesMode) {
const EGLint contextAttributes[] = {
EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};
m_context = eglCreateContext(
m_eglDisplay, m_config, EGL_NO_CONTEXT, contextAttributes);
} else {
m_context = eglCreateContext(
m_eglDisplay, m_config, EGL_NO_CONTEXT, nullptr);
}
if (m_context == EGL_NO_CONTEXT) {
qWarning(wpLog) << "WallpaperService: eglCreateContext failed";
return;
}
}
while (!m_stop) {
present();
if (wl_display_prepare_read(m_display) == 0) {
wl_display_flush(m_display);
struct pollfd fds[2] = {
{wl_display_get_fd(m_display), POLLIN, 0},
{m_wake[0], POLLIN, 0},
};
int ready = poll(fds, 2, kWaitTimeoutMs);
if (ready > 0 && fds[0].revents) {
wl_display_read_events(m_display);
continue;
}
if (ready > 0 && fds[1].revents) {
char byte = 0;
::read(m_wake[0], &byte, 1);
}
wl_display_cancel_read(m_display);
continue;
}
struct pollfd fds[1] = {{m_wake[0], POLLIN, 0}};
if (poll(fds, 1, kWaitTimeoutMs) <= 0) continue;
char byte = 0;
::read(m_wake[0], &byte, 1);
}
}
void WallpaperService::present() {
wl_display_dispatch_queue_pending(m_display, m_queue);
addSurfaces();
ensureImage();
ensurePoster();
if (m_isVideo && m_videoReady.load(std::memory_order_relaxed)) {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_replacementReady = true;
}
QList<WallpaperSurface*> surfaces;
bool videoReload = false;
QString path;
bool isVideo = false;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
videoReload = m_videoReload;
path = m_path;
isVideo = m_isVideo;
const bool animating = !m_transitions.empty();
const bool newVideoFrame =
m_isVideo &&
m_mpvFrameReady.exchange(false, std::memory_order_relaxed);
if (animating || newVideoFrame)
for (auto* surface : m_surfaces)
surface->markDirty();
surfaces = m_surfaces;
}
if (m_context == EGL_NO_CONTEXT) return;
EGLSurface importTarget = EGL_NO_SURFACE;
for (auto* surface : surfaces)
if (surface->eglSurface() != EGL_NO_SURFACE) {
importTarget = surface->eglSurface();
break;
}
if (importTarget == EGL_NO_SURFACE) return;
if (!eglMakeCurrent(m_eglDisplay, importTarget, importTarget, m_context)) {
qWarning(wpLog) << "WallpaperService: the context cannot be made "
"current, eglGetError = 0x"
<< Qt::hex << unsigned(eglGetError());
return;
}
if (videoReload) {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_videoReload = false;
}
if (!m_orphanTextures.empty()) {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
for (uint32_t tex : m_orphanTextures)
glDeleteTextures(1, &tex);
m_orphanTextures.clear();
}
const bool holdVideoForBase = (m_needCaptureBase && m_baseIsVideo) ||
m_collapseVideo;
auto applyVideoBackendSwitch = [&]() {
if (!videoReload) return;
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (isVideo) {
startVideo(path);
} else if (m_mpv != nullptr) {
teardownMpv();
}
#ifdef ZHAVE_VIDEO
else if (m_player != nullptr) {
m_player->deleteTexture();
stopPlayerVideo();
}
#endif
};
if (!holdVideoForBase) applyVideoBackendSwitch();
#ifdef ZHAVE_VIDEO
const bool videoActive =
(m_mpv != nullptr || m_player != nullptr) &&
(m_isVideo || m_baseIsVideo || m_collapseVideo || m_promoteOldest);
#else
const bool videoActive =
m_mpv != nullptr &&
(m_isVideo || m_baseIsVideo || m_collapseVideo || m_promoteOldest);
#endif
if (videoActive && m_mpv != nullptr) pumpMpvEvents();
#ifdef ZHAVE_VIDEO
if (videoActive && m_player != nullptr) m_player->importCurrentFrame();
#endif
for (auto* surface : surfaces)
drawSurface(surface);
if (holdVideoForBase) applyVideoBackendSwitch();
{
std::lock_guard<std::recursive_mutex> 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<int>(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() {
if (!started || !m_layerShell || !m_viewporter || !m_scaleManager) return;
QList<WallpaperSurface*> gone;
QList<QString> needed;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
QList<WallpaperSurface*> kept;
for (auto* surface : m_surfaces) {
if (m_screens.contains(surface->screen()))
kept.append(surface);
else
gone.append(surface);
}
m_surfaces = kept;
for (const auto& screen : m_screens) {
bool exists = false;
for (auto* surface : m_surfaces) {
if (surface->screen() == screen) {
exists = true;
break;
}
}
if (!exists) needed.append(screen);
}
}
for (auto* surface : gone)
delete surface;
for (const auto& screen : needed)
addSurface(screen);
}
void WallpaperService::registryGlobal(
void* data,
struct wl_registry* registry,
uint32_t name,
const char* interface,
uint32_t version) {
auto* service = static_cast<WallpaperService*>(data);
void* proxy = nullptr;
if (strcmp(interface, "zwlr_layer_shell_v1") == 0) {
proxy = new QtWayland::zwlr_layer_shell_v1(
registry, int(name), int(version));
} else if (strcmp(interface, "wp_viewporter") == 0) {
proxy = new QtWayland::wp_viewporter(registry, int(name), int(version));
} else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0) {
proxy = new QtWayland::wp_fractional_scale_manager_v1(
registry, int(name), int(version));
} else {
return;
}
service->storeGlobal(interface, proxy);
service->queueAll();
}
void WallpaperService::storeGlobal(const char* interface, void* proxy) {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (strcmp(interface, "zwlr_layer_shell_v1") == 0)
m_layerShell = static_cast<QtWayland::zwlr_layer_shell_v1*>(proxy);
else if (strcmp(interface, "wp_viewporter") == 0)
m_viewporter = static_cast<QtWayland::wp_viewporter*>(proxy);
else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0)
m_scaleManager =
static_cast<QtWayland::wp_fractional_scale_manager_v1*>(proxy);
}
void WallpaperService::bindGlobals() {
m_registry = wl_display_get_registry(m_display);
static const struct wl_registry_listener listener = {
registryGlobal,
nullptr,
};
wl_registry_add_listener(m_registry, &listener, this);
}
bool WallpaperService::setupGl() {
const char* extensions = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
if (extensions && strstr(extensions, "EGL_EXT_platform_wayland")) {
m_eglDisplay =
eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_EXT, m_display, nullptr);
} else {
m_eglDisplay =
eglGetDisplay(reinterpret_cast<EGLNativeDisplayType>(m_display));
}
if (m_eglDisplay == EGL_NO_DISPLAY) {
qWarning(wpLog) << "WallpaperService: no EGL display";
return false;
}
EGLint major = 0, minor = 0;
if (!eglInitialize(m_eglDisplay, &major, &minor)) {
qWarning(wpLog) << "WallpaperService: eglInitialize failed";
return false;
}
const EGLint configAttributes[] = {
EGL_SURFACE_TYPE,
EGL_WINDOW_BIT,
EGL_RENDERABLE_TYPE,
kGlesMode ? EGL_OPENGL_ES2_BIT : (EGL_OPENGL_BIT | EGL_OPENGL_ES2_BIT),
EGL_RED_SIZE,
8,
EGL_GREEN_SIZE,
8,
EGL_BLUE_SIZE,
8,
EGL_ALPHA_SIZE,
8,
EGL_NONE};
EGLint count = 0;
if (!eglChooseConfig(m_eglDisplay, configAttributes, &m_config, 1, &count) ||
count == 0) {
qWarning(wpLog)
<< "WallpaperService: no EGL config for window surfaces";
return false;
}
return true;
}
uint32_t WallpaperService::createProgram() {
const QByteArray vsSrc = shaderSource("wallpaper.vert", false);
const QByteArray fsSrc = shaderSource("wallpaper.frag", 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, "crop") < 0 ||
glGetUniformLocation(program, "alpha") < 0 ||
glGetUniformLocation(program, "source") < 0) {
qWarning(wpLog) << "WallpaperService: the program does not have the "
"uniforms it needs";
glDeleteProgram(program);
return 0;
}
m_sizeUniform = glGetUniformLocation(program, "size");
m_cropUniform = glGetUniformLocation(program, "crop");
m_alphaUniform = glGetUniformLocation(program, "alpha");
m_sourceUniform = glGetUniformLocation(program, "source");
glUseProgram(program);
glUniform1i(GLint(m_sourceUniform), 0);
glDeleteShader(vertex);
glDeleteShader(fragment);
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<double> 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);
const QByteArray fsSrc = shaderSource("wallpaper_video.frag", 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, "srcSize") < 0 ||
glGetUniformLocation(program, "crop") < 0 ||
glGetUniformLocation(program, "alpha") < 0 ||
glGetUniformLocation(program, "luma") < 0 ||
glGetUniformLocation(program, "chroma") < 0 ||
glGetUniformLocation(program, "matrix") < 0 ||
glGetUniformLocation(program, "range") < 0 ||
glGetUniformLocation(program, "chromaOff") < 0) {
qWarning(wpLog)
<< "WallpaperService: the video program does not have the "
"uniforms it needs";
glDeleteProgram(program);
return 0;
}
m_videoSizeUniform = glGetUniformLocation(program, "size");
m_videoSrcSizeUniform = glGetUniformLocation(program, "srcSize");
m_videoCropUniform = glGetUniformLocation(program, "crop");
m_videoAlphaUniform = glGetUniformLocation(program, "alpha");
m_videoLumaUniform = glGetUniformLocation(program, "luma");
m_videoChromaUniform = glGetUniformLocation(program, "chroma");
m_videoMatrixUniform = glGetUniformLocation(program, "matrix");
m_videoRangeUniform = glGetUniformLocation(program, "range");
m_videoChromaOffUniform = glGetUniformLocation(program, "chromaOff");
glDeleteShader(vertex);
glDeleteShader(fragment);
return program;
}
void WallpaperService::drawVideoTexture(
uint32_t lumaTex,
uint32_t chromaTex,
int matrix,
int range,
const QRectF& rect,
float sizeW,
float sizeH,
float srcW,
float srcH,
float alpha) {
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, lumaTex);
glUniform1i(GLint(m_videoLumaUniform), 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, chromaTex);
glUniform1i(GLint(m_videoChromaUniform), 1);
glUniform1i(GLint(m_videoMatrixUniform), matrix);
glUniform1i(GLint(m_videoRangeUniform), range);
glUniform2f(
GLint(m_videoChromaOffUniform),
range == 0 && srcW > 0 ? 0.5f / srcW : 0.0f,
0.0f);
glUniform2f(GLint(m_videoSizeUniform), sizeW, sizeH);
glUniform2f(GLint(m_videoSrcSizeUniform), srcW, srcH);
glUniform4f(
GLint(m_videoCropUniform),
GLfloat(rect.x()),
GLfloat(rect.y()),
GLfloat(rect.width()),
GLfloat(rect.height()));
glUniform1f(GLint(m_videoAlphaUniform), alpha);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
bool WallpaperService::startPlayerVideo(const QString& path) {
if (!m_player) m_player = std::make_unique<VideoPlayer>(m_eglDisplay);
m_player->setReadyCallback([this]() {
m_videoLoaded.store(true, std::memory_order_relaxed);
m_videoReady.store(true, std::memory_order_relaxed);
queueAll();
});
m_player->setFrameCallback([this]() {
m_videoReady.store(true, std::memory_order_relaxed);
queueAll();
});
if (!m_player->start(path)) return false;
return true;
}
void WallpaperService::stopPlayerVideo() {
if (!m_player) return;
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;
if (texture == 0) {
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
} else {
glBindTexture(GL_TEXTURE_2D, texture);
}
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_RGBA8,
image.width(),
image.height(),
0,
GL_RGBA,
GL_UNSIGNED_BYTE,
image.constBits());
}
void WallpaperService::uploadImage() {
uploadTexture(m_texture, m_image);
}
void WallpaperService::drawTexture(
uint32_t texture,
const QRectF& rect,
float sizeW,
float sizeH,
float alpha) {
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glUniform1i(GLint(m_sourceUniform), 0);
glUniform2f(GLint(m_sizeUniform), sizeW, sizeH);
glUniform4f(
GLint(m_cropUniform),
GLfloat(rect.x()),
GLfloat(rect.y()),
GLfloat(rect.width()),
GLfloat(rect.height()));
glUniform1f(GLint(m_alphaUniform), alpha);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
QByteArray WallpaperService::cachedBlob(const QString& path) const {
if (path.isEmpty()) return {};
QString databasePath;
{
std::lock_guard<std::recursive_mutex> 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 cache, size, mtime FROM wallpapers WHERE path = ?");
query.addBindValue(info.absoluteFilePath());
if (!query.exec() || !query.next()) return {};
const QByteArray blob = query.value(0).toByteArray();
if (blob.isEmpty()) return {};
const qint64 mtime = info.lastModified().toMSecsSinceEpoch() / 1000;
if (query.value(1).toLongLong() != info.size() ||
query.value(2).toLongLong() != mtime) {
return {};
}
return blob;
}
QByteArray WallpaperService::posterBlob(const QString& path) const {
if (path.isEmpty()) return {};
QString databasePath;
{
std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> 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 {};
QImageReader probe(path);
const QSize source = probe.size();
QSize required = source.isEmpty() ? decodeSize : source;
if (!source.isEmpty() && !decodeSize.isEmpty()) {
const QSize cover =
source.scaled(decodeSize, Qt::KeepAspectRatioByExpanding);
if (cover.width() <= source.width() &&
cover.height() <= source.height())
required = cover;
}
const QByteArray blob = cachedBlob(path);
if (!blob.isEmpty()) {
QImage image = QImage::fromData(blob, "JPEG");
if (!image.isNull() && image.width() >= required.width() &&
image.height() >= required.height()) {
return image.convertToFormat(QImage::Format_RGBA8888);
}
}
QImageReader reader(path);
reader.setAutoTransform(true);
if (!decodeSize.isEmpty() && !source.isEmpty())
reader.setScaledSize(
source.scaled(decodeSize, Qt::KeepAspectRatioByExpanding));
QImage image = reader.read();
if (image.isNull()) {
qWarning(wpLog) << "WallpaperService: could not read the image" << path;
return {};
}
return image.convertToFormat(QImage::Format_RGBA8888);
}
void WallpaperService::ensureImage() {
QSize largest;
QString path;
bool needDecode = false;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (m_isVideo) return;
largest = largestBufferSize();
if (largest.isEmpty()) return;
path = m_path;
if (m_stagedReady && m_stagedPath == path) {
m_image = m_stagedImage;
m_stagedImage = QImage();
m_stagedReady = false;
m_decodeSize = largest;
m_imageChanged = true;
m_replacementReady = true;
} else {
m_stagedReady = false;
m_stagedImage = QImage();
const bool grew = m_decodeSize.isEmpty() ||
largest.width() > m_decodeSize.width() ||
largest.height() > m_decodeSize.height();
if (!m_decoding && (m_forceDecode || path != m_decodePath ||
m_image.isNull() || grew)) {
m_forceDecode = false;
m_decoding = true;
m_decodePath = path;
needDecode = true;
} else {
m_forceDecode = false;
}
}
}
if (!needDecode) return;
QThreadPool::globalInstance()->start([this, path, largest]() {
const QImage image = decodeImage(path, largest);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_stagedImage = image;
m_stagedPath = path;
m_stagedReady = !image.isNull();
m_decoding = false;
});
wake();
}
void WallpaperService::ensurePoster() {
QString path;
{
std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> lock(m_mutex);
if (m_isVideo) return;
m_forceDecode = true;
}
wake();
}
QSize WallpaperService::largestBufferSize() const {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
QSize largest;
for (auto* surface : m_surfaces) {
QSize buffer = surface->bufferSize();
if (buffer.isEmpty()) continue;
if (largest.isEmpty() || qint64(buffer.width()) * buffer.height() >
qint64(largest.width()) * largest.height())
largest = buffer;
}
return largest;
}
void WallpaperService::notifyBufferSizes() {
QMetaObject::invokeMethod(
this, "largestBufferSizeChanged", Qt::QueuedConnection);
}
QRectF WallpaperService::sourceRect(
const QVariantMap& crops,
const QString& screen,
const QSize& size,
const QSize& image) const {
QVariantMap crop = cropFor(crops, screen);
QRectF rect(
crop.value("x", 0.0).toDouble(),
crop.value("y", 0.0).toDouble(),
crop.value("width", 1.0).toDouble(),
crop.value("height", 1.0).toDouble());
if (size.isEmpty() || rect.isEmpty() || image.isEmpty()) return rect;
qreal imageWidth = qreal(image.width());
qreal imageHeight = qreal(image.height());
QRectF pixels(
rect.x() * imageWidth,
rect.y() * imageHeight,
rect.width() * imageWidth,
rect.height() * imageHeight);
qreal targetRatio = qreal(size.width()) / qreal(size.height());
qreal rectRatio = pixels.width() / pixels.height();
if (rectRatio > targetRatio) {
qreal newWidth = pixels.height() * targetRatio;
pixels.setX(pixels.x() + (pixels.width() - newWidth) / 2.0);
pixels.setWidth(newWidth);
} else if (rectRatio < targetRatio) {
qreal newHeight = pixels.width() / targetRatio;
pixels.setY(pixels.y() + (pixels.height() - newHeight) / 2.0);
pixels.setHeight(newHeight);
}
return QRectF(
pixels.x() / imageWidth,
pixels.y() / imageHeight,
pixels.width() / imageWidth,
pixels.height() / imageHeight);
}
void WallpaperService::drawSurface(WallpaperSurface* surface) {
if (!m_transitions.empty()) {
drawSurfaceTransition(surface);
return;
}
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};
QVariantMap crops;
QSize size;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
crops = m_crops;
size = surface->bufferSize();
if (size.isEmpty()) return;
if (m_program == 0) m_program = createProgram();
if (m_program == 0) return;
const bool videoNow = m_isVideo;
if (videoNow && !m_videoReady.load(std::memory_order_relaxed)) return;
#ifdef ZHAVE_VIDEO
const bool videoAsBackground =
videoNow && (m_mpv != nullptr || m_player != nullptr);
#else
const bool videoAsBackground = videoNow && m_mpv != nullptr;
#endif
if (videoAsBackground) {
glDisable(GL_BLEND);
glViewport(0, 0, size.width(), size.height());
#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) return;
renderMpvToSurface(size);
}
} else {
glUseProgram(m_program);
glViewport(0, 0, size.width(), size.height());
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
if (!videoNow) {
const bool upload = m_imageChanged;
m_imageChanged = false;
if (m_texture == 0 || upload) uploadImage();
if (m_texture == 0 || m_image.isNull()) return;
}
glUseProgram(m_program);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
const float w = float(size.width());
const float h = float(size.height());
if (!videoNow) {
const QRectF rect =
sourceRect(crops, surface->screen(), size, m_image.size());
drawTexture(m_texture, rect, w, h, 1.0f);
}
}
surface->requestFrame();
if (!eglSwapBuffers(m_eglDisplay, target)) {
qWarning(wpLog)
<< "WallpaperService: eglSwapBuffers failed, eglGetError = 0x"
<< QString::number(unsigned(eglGetError()), 16);
return;
}
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<std::recursive_mutex> 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();
}
void WallpaperService::addSurface(const QString& screen) {
if (!m_layerShell || !m_viewporter || !m_scaleManager) return;
QScreen* qscreen = nullptr;
for (auto* candidate : QGuiApplication::screens()) {
if (candidate->name() == screen) {
qscreen = candidate;
break;
}
}
if (!qscreen) {
qWarning(wpLog) << "WallpaperService:" << screen << "is not a screen";
return;
}
auto* waylandScreen =
dynamic_cast<QtWaylandClient::QWaylandScreen*>(qscreen->handle());
if (!waylandScreen || !waylandScreen->output()) {
qWarning(wpLog) << "WallpaperService:" << screen
<< "is not a Wayland screen";
return;
}
wl_surface* wlSurface = wl_compositor_create_surface(m_compositor);
if (!wlSurface) return;
auto* surface =
new WallpaperSurface(this, wlSurface, waylandScreen->output(), screen);
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_surfaces.append(surface);
}
notifyBufferSizes();
}
} // namespace ZShell::internal