#define EGL_EGLEXT_PROTOTYPES #define GL_GLEXT_PROTOTYPES #include "wallpaperservice.hpp" #include #include #include #ifndef GL_TEXTURE_EXTERNAL_OES #define GL_TEXTURE_EXTERNAL_OES 0x8D66 #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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(); } 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) {} 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); 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); #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 lock(m_mutex); return m_path; } QString WallpaperService::dbPath() const { std::lock_guard lock(m_mutex); return m_dbPath; } void WallpaperService::setDbPath(const QString& path) { { std::lock_guard 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 lock(m_mutex); m_path = path; #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; m_previousVideo = wasVideo && !video; if (wasImage) { if (m_previousTexture != 0) m_orphanTextures.push_back(m_previousTexture); m_previousImage = m_image; m_previousCrops = m_crops; m_previousTexture = 0; } else { m_previousImage = QImage(); } m_isVideo = video; m_fading = true; m_fadeClockStarted = false; m_replacementReady = 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(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 lock(m_mutex); if (m_screens.contains(screen)) return; m_screens.append(screen); } start(); queueAll(); } void WallpaperService::removeScreen(const QString& screen) { { std::lock_guard lock(m_mutex); if (!m_screens.removeOne(screen)) return; } queueAll(); } const QVariantMap& WallpaperService::crops() const { std::lock_guard lock(m_mutex); return m_crops; } void WallpaperService::setCrops(const QVariantMap& crops) { { std::lock_guard 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 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(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( +[](GLenum, GLbitfield) -> GLsync { return nullptr; }); return reinterpret_cast(eglGetProcAddress(name)); } void WallpaperService::mpvUpdate(void* ctx) { auto* self = static_cast(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(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 lock(m_mutex); if (started || m_path.isEmpty() || m_screens.isEmpty()) return; started = true; } auto* interface = qApp->nativeInterface(); 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 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(); if (m_isVideo && m_videoReady.load(std::memory_order_relaxed)) { std::lock_guard lock(m_mutex); m_replacementReady = true; } QList surfaces; bool fading = false; bool videoReload = false; QString path; bool isVideo = false; { std::lock_guard lock(m_mutex); videoReload = m_videoReload; 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; 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 lock(m_mutex); m_videoReload = false; } if (!m_orphanTextures.empty()) { std::lock_guard lock(m_mutex); for (uint32_t tex : m_orphanTextures) glDeleteTextures(1, &tex); m_orphanTextures.clear(); } if (videoReload) { if (isVideo) startVideo(path); else if (m_mpv && !m_previousVideo) teardownMpv(); #ifdef ZHAVE_VIDEO else if (m_player && !m_previousVideo) { m_player->deleteTexture(); stopPlayerVideo(); } #endif } #ifdef ZHAVE_VIDEO const bool videoActive = (m_isVideo || m_previousVideo) && (m_mpv != nullptr || m_player != nullptr); #else const bool videoActive = (m_isVideo || m_previousVideo) && m_mpv != nullptr; #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); } void WallpaperService::addSurfaces() { if (!started || !m_layerShell || !m_viewporter || !m_scaleManager) return; QList gone; QList needed; { std::lock_guard lock(m_mutex); QList 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(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 lock(m_mutex); if (strcmp(interface, "zwlr_layer_shell_v1") == 0) m_layerShell = static_cast(proxy); else if (strcmp(interface, "wp_viewporter") == 0) m_viewporter = static_cast(proxy); else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0) m_scaleManager = static_cast(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(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; } #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(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(); } #endif 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 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; } 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 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 lock(m_mutex); m_stagedImage = image; m_stagedPath = path; m_stagedReady = !image.isNull(); m_decoding = false; }); wake(); } void WallpaperService::invalidateCache() { { std::lock_guard lock(m_mutex); if (m_isVideo) return; m_forceDecode = true; } wake(); } QSize WallpaperService::largestBufferSize() const { std::lock_guard 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 (!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 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_previousVideo && m_fading)) && (m_mpv != nullptr || m_player != nullptr); #else const bool videoAsBackground = (videoNow || (m_previousVideo && m_fading)) && 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); if (m_fading && !m_fadeClockStarted) { m_fadeClockStarted = true; m_fadeTimer.start(); } bool fade = false; bool fadeDone = false; qreal progress = 1.0; if (m_fading) { progress = m_fadeClockStarted ? qreal(m_fadeTimer.elapsed()) / qreal(kFadeDurationMs) : 0.0; if (progress >= 1.0 && m_replacementReady) fadeDone = true; else fade = true; } const float previousAlpha = m_replacementReady ? float(1.0 - progress) : 1.0f; const float replacementAlpha = m_replacementReady ? float(progress) : 0.0f; const float w = float(size.width()); const float h = float(size.height()); if (videoNow) { if (fade && !m_previousImage.isNull()) { if (m_previousTexture == 0) uploadTexture(m_previousTexture, m_previousImage); if (m_previousTexture != 0) { const QRectF previousRect = sourceRect( m_previousCrops, surface->screen(), size, m_previousImage.size()); drawTexture( m_previousTexture, previousRect, w, h, previousAlpha); } } else if (fade && !m_firstFadeDone) { if (m_blackTex == 0) createBlackTexture(m_blackTex); if (m_blackTex != 0) drawTexture( m_blackTex, QRectF(0.0, 0.0, 1.0, 1.0), w, h, previousAlpha); } } else { const QRectF rect = sourceRect(crops, surface->screen(), size, m_image.size()); if (fade) { if (!m_previousVideo && !m_previousImage.isNull() && m_previousTexture == 0) { uploadTexture(m_previousTexture, m_previousImage); } if (!m_previousVideo && m_previousTexture != 0) { const QRectF previousRect = sourceRect( m_previousCrops, surface->screen(), size, m_previousImage.size()); drawTexture(m_previousTexture, previousRect, w, h, 1.0f); } drawTexture(m_texture, rect, w, h, replacementAlpha); } else { drawTexture(m_texture, rect, w, h, 1.0f); } } if (fadeDone) { m_fading = false; m_firstFadeDone = true; if (m_previousVideo) { teardownMpv(); #ifdef ZHAVE_VIDEO if (m_player) { m_player->deleteTexture(); stopPlayerVideo(); } #endif m_previousVideo = false; } if (m_previousTexture != 0) { glDeleteTextures(1, &m_previousTexture); m_previousTexture = 0; } m_previousImage = QImage(); } } surface->requestFrame(); if (!eglSwapBuffers(m_eglDisplay, target)) { qWarning(wpLog) << "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(wpLog) << "WallpaperService:" << screen << "is not a screen"; return; } auto* waylandScreen = dynamic_cast(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 lock(m_mutex); m_surfaces.append(surface); } notifyBufferSizes(); } } // namespace ZShell::internal