Files
z-bar-qt/Plugins/ZShell/Internal/wallpaperservice.cpp
T

1337 lines
30 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>
#include <mpv/client.h>
#include <mpv/render_gl.h>
#include <clocale>
#include <fcntl.h>
#include <poll.h>
#include <unistd.h>
#include <QDebug>
#include <QFileInfo>
#include <QGuiApplication>
#include <QImageReader>
#include <QScreen>
#include <QCryptographicHash>
#include <QDir>
#include <QFile>
#include <QProcess>
#include <qguiapplication_platform.h>
#include <private/qwaylandscreen_p.h>
#include <cstring>
namespace ZShell::internal {
namespace {
const char* kVertexShader = R"(
#version 300 es
void main() {
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
}
)";
const char* kFragmentShader = R"(
#version 300 es
precision highp float;
uniform sampler2D source;
uniform vec2 size;
uniform vec4 crop;
uniform float alpha;
out vec4 outColor;
void main() {
vec2 p = vec2(gl_FragCoord.x, size.y - gl_FragCoord.y) / size;
vec4 c = texture(source, crop.xy + p * crop.zw);
outColor = vec4(c.rgb, c.a * alpha);
}
)";
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() << "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) {}
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);
if (m_previousTexture) glDeleteTextures(1, &m_previousTexture);
if (m_blackTex) glDeleteTextures(1, &m_blackTex);
if (m_program) glDeleteProgram(m_program);
}
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;
}
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);
m_path = path;
const bool wasVideo = m_isVideo && m_mpv != nullptr;
const bool wasImage = !m_image.isNull() && m_texture != 0;
m_previousVideo = wasVideo && !video;
if (wasImage) {
m_previousImage = m_image;
m_previousCrops = m_crops;
m_previousTexture = 0;
} else {
m_previousImage = QImage();
}
m_isVideo = video;
m_fading = true;
m_fadeClockStarted = false;
m_forceDecode = true;
m_imageChanged = true;
m_videoReload = true;
}
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() << "WallpaperService: ffprobe failed for" << path << "(exit"
<< probe.exitCode() << ")";
return {};
}
const QString codec =
QString::fromUtf8(probe.readAllStandardOutput()).trimmed().toLower();
qWarning() << "WallpaperService: codec of" << path << "is"
<< (codec.isEmpty() ? "<empty>" : codec);
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().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() << "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() << "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);
if (!m_mpv) {
std::setlocale(LC_NUMERIC, "C");
m_mpv = mpv_create();
if (!m_mpv) {
qWarning() << "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() << "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() << "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_LOG_MESSAGE) {
auto* data = static_cast<mpv_event_log_message*>(event->data);
if (data && data->text)
qWarning().noquote() << "mpv:" << data->prefix << data->text;
}
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() << "WallpaperService: mpv ended the file, error"
<< data->error;
}
}
}
void WallpaperService::stopVideo() {
teardownMpv();
}
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},
};
if (mpv_render_context_create(&m_mpvRender, m_mpv, params) < 0) {
qWarning() << "WallpaperService: could not create mpv render context";
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() << "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() << "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() << "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() {
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();
ensureDecodeSize();
QList<WallpaperSurface*> surfaces;
bool fading = false;
bool videoReload = false;
QString path;
bool isVideo = false;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (m_videoReload) {
m_videoReload = false;
videoReload = true;
path = m_path;
isVideo = m_isVideo;
}
fading = m_fading;
const bool newVideoFrame =
m_isVideo &&
m_mpvFrameReady.exchange(false, std::memory_order_relaxed);
if (fading || newVideoFrame)
for (auto* surface : m_surfaces)
surface->markDirty();
surfaces = m_surfaces;
}
if (m_context == EGL_NO_CONTEXT) return;
eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, m_context);
if (videoReload) {
if (isVideo)
startVideo(path);
else if (m_mpv && !m_previousVideo)
teardownMpv();
}
const bool videoActive = (m_isVideo || m_previousVideo) && m_mpv != nullptr;
if (videoActive) pumpMpvEvents();
for (auto* surface : surfaces)
drawSurface(surface);
}
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() << "WallpaperService: no EGL display";
return false;
}
EGLint major = 0, minor = 0;
if (!eglInitialize(m_eglDisplay, &major, &minor)) {
qWarning() << "WallpaperService: eglInitialize failed";
return false;
}
const EGLint configAttributes[] = {
EGL_SURFACE_TYPE,
EGL_WINDOW_BIT,
EGL_RENDERABLE_TYPE,
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() << "WallpaperService: no EGL config for window surfaces";
return false;
}
const EGLint contextAttributes[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};
m_context = eglCreateContext(
m_eglDisplay, m_config, EGL_NO_CONTEXT, contextAttributes);
if (m_context == EGL_NO_CONTEXT) {
qWarning() << "WallpaperService: eglCreateContext failed";
return false;
}
return true;
}
uint32_t WallpaperService::createProgram() {
uint32_t vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &kVertexShader, nullptr);
glCompileShader(vertex);
logShader(vertex, false);
uint32_t fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &kFragmentShader, 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) {
qWarning() << "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");
glDeleteShader(vertex);
glDeleteShader(fragment);
return program;
}
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) {
glBindTexture(GL_TEXTURE_2D, texture);
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);
}
bool WallpaperService::decode(const QString& path) {
if (path.isEmpty()) return false;
QImageReader reader(path);
reader.setAutoTransform(true);
QSize size = reader.size();
if (size.isValid()) m_sourceSize = size;
if (!m_decodeSize.isEmpty() && !m_sourceSize.isEmpty())
reader.setScaledSize(
m_sourceSize.scaled(m_decodeSize, Qt::KeepAspectRatioByExpanding));
QImage image = reader.read();
if (image.isNull()) {
qWarning() << "WallpaperService: could not read the image" << path;
return false;
}
m_image = image.convertToFormat(QImage::Format_RGBA8888);
return !m_image.isNull();
}
bool WallpaperService::ensureDecodeSize() {
QSize largest;
QString path;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (m_isVideo) return false;
largest = largestBufferSize();
if (largest.isEmpty()) return false;
if (!m_forceDecode && !m_decodeSize.isEmpty() &&
qint64(largest.width()) * largest.height() <=
qint64(m_decodeSize.width()) * m_decodeSize.height())
return false;
QSize target =
m_sourceSize.isEmpty()
? largest
: m_sourceSize.scaled(largest, Qt::KeepAspectRatioByExpanding);
if (!m_forceDecode && !m_image.isNull() && target == m_image.size())
return false;
m_forceDecode = false;
m_decodeSize = largest;
path = m_path;
}
if (!decode(path)) return false;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_imageChanged = true;
}
return true;
}
QSize WallpaperService::largestBufferSize() {
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;
}
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 (!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() << "WallpaperService: the context cannot be made current";
return;
}
eglSwapInterval(m_eglDisplay, 0);
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;
const bool videoAsBackground =
(videoNow || (m_previousVideo && m_fading)) && m_mpv != nullptr;
if (videoAsBackground) {
if (!m_mpvRender) initMpvRender();
if (!m_mpvRender) return;
glDisable(GL_BLEND);
glViewport(0, 0, size.width(), size.height());
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);
if (m_fading && !m_fadeClockStarted) {
m_fadeClockStarted = true;
m_fadeTimer.start();
}
bool fade = false;
bool fadeDone = false;
qreal progress = 1.0;
if (m_fading) {
progress = qreal(m_fadeTimer.elapsed()) / qreal(kFadeDurationMs);
if (progress >= 1.0)
fadeDone = true;
else
fade = true;
}
const float w = float(size.width());
const float h = float(size.height());
if (videoNow) {
if (fade && !m_previousImage.isNull()) {
if (m_previousTexture == 0)
uploadTexture(m_previousTexture, m_previousImage);
if (m_previousTexture != 0) {
const QRectF previousRect = sourceRect(
m_previousCrops,
surface->screen(),
size,
m_previousImage.size());
drawTexture(
m_previousTexture,
previousRect,
w,
h,
1.0f - float(progress));
}
} else if (fade && !m_firstFadeDone) {
if (m_blackTex == 0) createBlackTexture(m_blackTex);
if (m_blackTex != 0)
drawTexture(
m_blackTex,
QRectF(0.0, 0.0, 1.0, 1.0),
w,
h,
1.0f - float(progress));
}
} else {
const QRectF rect =
sourceRect(crops, surface->screen(), size, m_image.size());
if (fade) {
if (!m_previousVideo && !m_previousImage.isNull() &&
m_previousTexture == 0) {
uploadTexture(m_previousTexture, m_previousImage);
}
if (!m_previousVideo && m_previousTexture != 0) {
const QRectF previousRect = sourceRect(
m_previousCrops,
surface->screen(),
size,
m_previousImage.size());
drawTexture(m_previousTexture, previousRect, w, h, 1.0f);
}
drawTexture(m_texture, rect, w, h, float(progress));
} else {
drawTexture(m_texture, rect, w, h, 1.0f);
}
}
if (fadeDone) {
m_fading = false;
m_firstFadeDone = true;
if (m_previousVideo) {
teardownMpv();
m_previousVideo = false;
}
if (m_previousTexture != 0) {
glDeleteTextures(1, &m_previousTexture);
m_previousTexture = 0;
}
m_previousImage = QImage();
}
}
surface->requestFrame();
if (!eglSwapBuffers(m_eglDisplay, target)) {
qWarning()
<< "WallpaperService: eglSwapBuffers failed, eglGetError = 0x"
<< QString::number(unsigned(eglGetError()), 16);
return;
}
if ((m_isVideo || m_previousVideo) && 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() << "WallpaperService:" << screen << "is not a screen";
return;
}
auto* waylandScreen =
dynamic_cast<QtWaylandClient::QWaylandScreen*>(qscreen->handle());
if (!waylandScreen || !waylandScreen->output()) {
qWarning() << "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);
}
}
} // namespace ZShell::internal