#define EGL_EGLEXT_PROTOTYPES #define GL_GLEXT_PROTOTYPES #include "wallpaperservice.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 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 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() ? "" : 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 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); 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(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(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( +[](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}, }; 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 lock(m_mutex); if (started || m_path.isEmpty() || m_screens.isEmpty()) return; started = true; } auto* interface = qApp->nativeInterface(); 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 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 surfaces; bool fading = false; bool videoReload = false; QString path; bool isVideo = false; { std::lock_guard 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 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() << "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 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 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 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(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 lock(m_mutex); m_surfaces.append(surface); } } } // namespace ZShell::internal