#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 #include #include #include #include "config/background.hpp" #include "config/rootnodes.hpp" #include "config/services.hpp" #include namespace ZShell::wallpaper { Q_LOGGING_CATEGORY(wpLog, "ZShell.Internal.wallpaper") namespace { const bool kMpvMode = qEnvironmentVariableIsSet("ZBAR_WALLPAPER_MPV"); const bool kGlesMode = qEnvironmentVariableIsSet("ZBAR_WALLPAPER_GLES") || kMpvMode; VideoBackend backendFromConfig() { const auto type = ZShell::config::ConfigSingleton::instance()->services()->gpuType(); switch (type) { case ZShell::config::GpuType::Nvidia: return VideoBackend::Nvidia; case ZShell::config::GpuType::Generic: return VideoBackend::Vaapi; case ZShell::config::GpuType::None: return VideoBackend::None; case ZShell::config::GpuType::Auto: default: return VideoBackend::Auto; } } void logSelectedBackend() { qInfo(wpLog) << "WallpaperService: video backend =" << resolveBackendName(backendFromConfig()); } const char* shaderHeader(bool fragment) { if (kGlesMode) return fragment ? "#version 300 es\nprecision highp float;\n" : "#version 300 es\n"; return "#version 330\n"; } QByteArray shaderSource(const QString& file, bool fragment) { QFile source(QStringLiteral(":/shaders/") + file); if (!source.open(QIODevice::ReadOnly)) { qWarning(wpLog) << "WallpaperService: could not read the shader" << file; return {}; } return QByteArray(shaderHeader(fragment)) + source.readAll(); } const char* transitionFile(ZShell::config::WallpaperTransition::Enum value) { switch (value) { case ZShell::config::WallpaperTransition::Bowtie: return "wallpaper_trans_bowtie.frag"; case ZShell::config::WallpaperTransition::Fade: return "wallpaper_trans_fade.frag"; case ZShell::config::WallpaperTransition::LinearBlur: return "wallpaper_trans_linear_blur.frag"; case ZShell::config::WallpaperTransition::WindowSlices: return "wallpaper_trans_window_slices.frag"; case ZShell::config::WallpaperTransition::WipeRight: return "wallpaper_trans_wipe_right.frag"; case ZShell::config::WallpaperTransition::WipeDown: return "wallpaper_trans_wipe_down.frag"; case ZShell::config::WallpaperTransition::TilesWave: return "wallpaper_trans_tiles_wave.frag"; case ZShell::config::WallpaperTransition::SplitSlideInHorizontal: return "wallpaper_trans_split_slide_in_horizontal.frag"; case ZShell::config::WallpaperTransition::Hexagonalize: return "wallpaper_trans_hexagonalize.frag"; case ZShell::config::WallpaperTransition::DoomMelt: return "wallpaper_trans_doom_melt.frag"; case ZShell::config::WallpaperTransition::Random: break; } return "wallpaper_trans_fade.frag"; } const QStringList& transitionFilesList() { static const QStringList files = { "wallpaper_trans_bowtie.frag", "wallpaper_trans_fade.frag", "wallpaper_trans_linear_blur.frag", "wallpaper_trans_window_slices.frag", "wallpaper_trans_wipe_right.frag", "wallpaper_trans_wipe_down.frag", "wallpaper_trans_tiles_wave.frag", "wallpaper_trans_split_slide_in_horizontal.frag", "wallpaper_trans_hexagonalize.frag", "wallpaper_trans_doom_melt.frag", }; return files; } constexpr int kWaitTimeoutMs = 20; constexpr int kFadeDurationMs = 400; const QStringList kVideoExtensions = { "mp4", "mkv", "webm", "avi", "mov", "wmv", "flv", "m4v", "mpg", "mpeg", "3gp", "ogv", "ts", "vob", }; void logShader(uint32_t object, bool program) { int ok = 0; if (program) glGetProgramiv(object, GL_LINK_STATUS, &ok); else glGetShaderiv(object, GL_COMPILE_STATUS, &ok); if (ok) return; char info[512] = {}; if (program) glGetProgramInfoLog(object, int(sizeof(info)), nullptr, info); else glGetShaderInfoLog(object, int(sizeof(info)), nullptr, info); qWarning(wpLog) << "WallpaperService:" << (program ? "linking" : "compiling") << "failed:" << info; } QVariantMap cropFor(const QVariantMap& crops, const QString& screen) { QVariantMap crop = crops.value(screen).toMap(); if (crop.isEmpty()) { crop.insert("x", 0.0); crop.insert("y", 0.0); crop.insert("width", 1.0); crop.insert("height", 1.0); crop.insert("zoom", 1.0); } return crop; } } // namespace WallpaperService::WallpaperService(QObject* parent) : QObject(parent) { auto* background = ZShell::config::ConfigSingleton::instance()->background(); { std::lock_guard lock(m_mutex); m_randomTransition = background->wallTransition() == ZShell::config::WallpaperTransition::Random; m_transitionFile = transitionFileFor(background->wallTransition()); } QObject::connect( background, &ZShell::config::Background::wallTransitionChanged, this, [this, background] { { std::lock_guard lock(m_mutex); m_randomTransition = background->wallTransition() == ZShell::config::WallpaperTransition::Random; m_transitionFile = transitionFileFor(background->wallTransition()); m_transitionDirty = true; } wake(); }); m_transcoder = new VideoTranscoder(this); connect( m_transcoder, &VideoTranscoder::transcoded, this, [this](const QString& dst, bool ok) { if (ok) applyPath(dst); }); auto* gpuConfig = ZShell::config::ConfigSingleton::instance()->services(); logSelectedBackend(); QObject::connect( gpuConfig, &ZShell::config::Services::gpuTypeChanged, this, [] { logSelectedBackend(); }); } WallpaperService::~WallpaperService() { m_stop = true; wake(); if (m_thread.joinable()) m_thread.join(); if (m_mpvRender) { mpv_render_context_free(m_mpvRender); m_mpvRender = nullptr; } if (m_mpv) { mpv_terminate_destroy(m_mpv); m_mpv = nullptr; } for (auto* surface : m_surfaces) delete surface; m_surfaces.clear(); if (m_eglDisplay != EGL_NO_DISPLAY && m_context != EGL_NO_CONTEXT) { if (eglMakeCurrent( m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, m_context)) { if (m_texture) glDeleteTextures(1, &m_texture); for (const auto& layer : m_transitions) if (layer.tex) glDeleteTextures(1, &layer.tex); m_transitions.clear(); if (m_baseTex) glDeleteTextures(1, &m_baseTex); if (m_baseFbo) glDeleteFramebuffers(1, &m_baseFbo); if (m_accumA) glDeleteTextures(1, &m_accumA); if (m_accumB) glDeleteTextures(1, &m_accumB); if (m_accumFbo) glDeleteFramebuffers(1, &m_accumFbo); for (uint32_t tex : m_orphanTextures) glDeleteTextures(1, &tex); m_orphanTextures.clear(); if (m_blackTex) glDeleteTextures(1, &m_blackTex); if (m_program) glDeleteProgram(m_program); if (m_videoProgram) glDeleteProgram(m_videoProgram); for (const auto& p : m_transPrograms) if (p.id) glDeleteProgram(p.id); m_transPrograms.clear(); m_neutralProgram = TransProgram(); if (m_captureTex) glDeleteTextures(1, &m_captureTex); if (m_captureFbo) glDeleteFramebuffers(1, &m_captureFbo); if (m_posterTex) glDeleteTextures(1, &m_posterTex); #ifdef ZHAVE_FFMPEG if (m_decoder) { m_decoder->stop(); m_decoder->deleteTexture(); m_decoder.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; } m_cache.setDbPath(path); emit dbPathChanged(); wake(); } void WallpaperService::setPath(const QString& path) { if (path.isEmpty() || path == this->path()) return; if (isVideoPath(path) && m_transcoder->needsTranscode(path)) { const QString dst = m_transcoder->transcodeTarget(path); if (QFile::exists(dst)) { applyPath(dst); return; } applyPath(path); m_transcoder->start(path, dst); return; } applyPath(path); } void WallpaperService::applyPath(const QString& path) { const bool video = isVideoPath(path); { std::lock_guard lock(m_mutex); const QString previousPath = m_path; m_path = path; if (m_randomTransition) { const QString next = randomTransitionFile(); if (next != m_transitionFile) { m_transitionFile = next; m_transitionDirty = true; } } #ifdef ZHAVE_FFMPEG const bool videoBackend = m_mpv != nullptr || m_decoder != nullptr; #else const bool videoBackend = m_mpv != nullptr; #endif const bool wasVideo = m_isVideo && videoBackend; const bool wasImage = !m_image.isNull() && m_texture != 0; const bool beginBurst = m_transitions.empty(); if (beginBurst) { if (wasVideo) { m_needCaptureBase = true; m_baseIsVideo = true; m_baseIsBlack = false; m_baseImage = QImage(); m_baseCrops = m_cache.cropsForPath(previousPath); } else if (wasImage) { m_needCaptureBase = true; m_baseIsVideo = false; m_baseIsBlack = false; m_baseImage = m_image; m_baseCrops = m_cache.cropsForPath(previousPath); } else { m_needCaptureBase = false; m_baseIsVideo = false; m_baseIsBlack = m_baseTex == 0; m_baseImage = QImage(); m_baseCrops = m_cache.cropsForPath(previousPath); } } if (!beginBurst && !m_transitions.empty() && m_transitions.back().isVideo && !m_transitions.back().started && !m_posterReady) m_transitions.pop_back(); if (!beginBurst && !m_transitions.empty() && m_texture != 0 && !m_transitions.back().isVideo) { TransitionLayer& newest = m_transitions.back(); newest.tex = m_texture; newest.image = m_image; newest.crops = m_cache.cropsForPath(previousPath); newest.ready = true; m_texture = 0; m_image = QImage(); } if (!beginBurst && video && !m_transitions.empty() && m_transitions.back().isVideo) m_collapseVideo = true; m_crops = m_cache.cropsForPath(path); m_isVideo = video; m_replacementReady = false; m_forceDecode = true; m_imageChanged = true; m_videoReload = true; TransitionLayer layer; layer.isVideo = video; layer.transitionFile = m_transitionFile; m_transitions.push_back(layer); if (static_cast(m_transitions.size()) > kMaxTransitions) m_promoteOldest = true; while (static_cast(m_transitions.size()) > kMaxTransitions * 2) { if (m_transitions.front().tex != 0) m_orphanTextures.push_back(m_transitions.front().tex); m_transitions.erase(m_transitions.begin()); } } emit pathChanged(); start(); queueAll(); } 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_FFMPEG if (m_useDecoder && !kMpvMode) { if (startDecoderVideo(path)) return; qWarning(wpLog) << "WallpaperService: the GPU video player failed, " "falling back to mpv"; m_useDecoder = false; m_decoder.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(); ensurePoster(); if (m_isVideo && m_videoReady.load(std::memory_order_relaxed)) { std::lock_guard lock(m_mutex); m_replacementReady = true; } QList surfaces; bool videoReload = false; QString path; bool isVideo = false; { std::lock_guard lock(m_mutex); videoReload = m_videoReload; path = m_path; isVideo = m_isVideo; const bool animating = !m_transitions.empty(); const bool newVideoFrame = m_isVideo && m_mpvFrameReady.exchange(false, std::memory_order_relaxed); if (animating || newVideoFrame) for (auto* surface : m_surfaces) surface->markDirty(); surfaces = m_surfaces; } if (m_context == EGL_NO_CONTEXT) return; if (!m_orphanTextures.empty()) { std::lock_guard lock(m_mutex); if (eglMakeCurrent( m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, m_context)) { for (uint32_t tex : m_orphanTextures) glDeleteTextures(1, &tex); m_orphanTextures.clear(); } } 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 (!m_programsCompiled) { m_programsCompiled = true; std::lock_guard lock(m_mutex); if (m_program == 0) m_program = createProgram(); if (m_videoProgram == 0) m_videoProgram = createVideoProgram(); if (m_neutralProgram.id == 0) m_neutralProgram = programFor(QStringLiteral("wallpaper_trans_fade.frag")); for (const QString& file : transitionFilesList()) (void)programFor(file); } if (videoReload) { std::lock_guard lock(m_mutex); m_videoReload = false; } { std::lock_guard lock(m_mutex); for (uint32_t tex : m_orphanTextures) glDeleteTextures(1, &tex); m_orphanTextures.clear(); const bool idle = m_transitions.empty() && !m_needCaptureBase && !m_collapseVideo && !m_isVideo && m_mpv == nullptr #ifdef ZHAVE_FFMPEG && m_decoder == nullptr #endif ; if (idle) { if (m_baseTex) glDeleteTextures(1, &m_baseTex); m_baseTex = 0; m_baseSize = QSize(); if (m_baseFbo) glDeleteFramebuffers(1, &m_baseFbo); m_baseFbo = 0; m_baseReady = false; if (m_accumA) glDeleteTextures(1, &m_accumA); if (m_accumB) glDeleteTextures(1, &m_accumB); m_accumA = 0; m_accumB = 0; if (m_accumFbo) glDeleteFramebuffers(1, &m_accumFbo); m_accumFbo = 0; m_accumSize = QSize(); if (m_captureTex) glDeleteTextures(1, &m_captureTex); m_captureTex = 0; if (m_captureFbo) glDeleteFramebuffers(1, &m_captureFbo); m_captureFbo = 0; m_captureSize = QSize(); if (m_posterTex) glDeleteTextures(1, &m_posterTex); m_posterTex = 0; m_posterReady = false; m_posterChanged = false; } } const bool holdVideoForBase = (m_needCaptureBase && m_baseIsVideo) || m_collapseVideo; auto applyVideoBackendSwitch = [&]() { if (!videoReload) return; std::lock_guard lock(m_mutex); if (isVideo) { startVideo(path); } else if (m_mpv != nullptr) { teardownMpv(); } #ifdef ZHAVE_FFMPEG else if (m_decoder != nullptr) { m_decoder->deleteTexture(); stopDecoderVideo(); } #endif }; if (!holdVideoForBase) applyVideoBackendSwitch(); #ifdef ZHAVE_FFMPEG const bool videoActive = (m_mpv != nullptr || m_decoder != nullptr) && (m_isVideo || m_baseIsVideo || m_collapseVideo || m_promoteOldest); #else const bool videoActive = m_mpv != nullptr && (m_isVideo || m_baseIsVideo || m_collapseVideo || m_promoteOldest); #endif if (videoActive && m_mpv != nullptr) pumpMpvEvents(); #ifdef ZHAVE_FFMPEG if (videoActive && m_decoder != nullptr) m_decoder->importCurrentFrame(); #endif for (auto* surface : surfaces) drawSurface(surface); if (holdVideoForBase) applyVideoBackendSwitch(); { std::lock_guard lock(m_mutex); if (m_collapseVideo) { for (size_t i = 0; i + 1 < m_transitions.size(); ++i) if (m_transitions[i].tex != 0) m_orphanTextures.push_back(m_transitions[i].tex); if (m_transitions.size() > 1) m_transitions.erase( m_transitions.begin(), m_transitions.end() - 1); m_collapseVideo = false; } if (m_promoteOldest && !m_transitions.empty()) { if (m_transitions.front().tex != 0) m_orphanTextures.push_back(m_transitions.front().tex); m_transitions.erase(m_transitions.begin()); m_promoteOldest = static_cast(m_transitions.size()) > kMaxTransitions; } if (m_newestDone) { for (const auto& layer : m_transitions) if (layer.tex != 0) m_orphanTextures.push_back(layer.tex); m_transitions.clear(); m_newestDone = false; m_baseIsBlack = false; m_baseImage = QImage(); m_baseCrops = QVariantMap(); } if (m_needCaptureBase) { m_needCaptureBase = false; m_baseIsVideo = false; } } } void WallpaperService::addSurfaces() { if (!started || !m_layerShell || !m_viewporter || !m_scaleManager) return; QList 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; } WallpaperService::TransProgram WallpaperService::createTransitionProgram( const QString& file) { TransProgram p; const QByteArray vsSrc = shaderSource("wallpaper.vert", false); const QByteArray fsSrc = shaderSource(file, true); const char* vs = vsSrc.constData(); const char* fs = fsSrc.constData(); uint32_t vertex = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertex, 1, &vs, nullptr); glCompileShader(vertex); logShader(vertex, false); uint32_t fragment = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragment, 1, &fs, nullptr); glCompileShader(fragment); logShader(fragment, false); uint32_t program = glCreateProgram(); glAttachShader(program, vertex); glAttachShader(program, fragment); glLinkProgram(program); logShader(program, true); if (glGetUniformLocation(program, "size") < 0 || glGetUniformLocation(program, "from") < 0 || glGetUniformLocation(program, "to") < 0 || glGetUniformLocation(program, "fromCrop") < 0 || glGetUniformLocation(program, "toCrop") < 0 || glGetUniformLocation(program, "fromFlip") < 0 || glGetUniformLocation(program, "toFlip") < 0 || glGetUniformLocation(program, "progress") < 0) { qWarning(wpLog) << "WallpaperService: the transition program does not " "have the uniforms it needs"; glDeleteProgram(program); glDeleteShader(vertex); glDeleteShader(fragment); return p; } p.id = program; p.size = glGetUniformLocation(program, "size"); p.progress = glGetUniformLocation(program, "progress"); p.from = glGetUniformLocation(program, "from"); p.to = glGetUniformLocation(program, "to"); p.fromCrop = glGetUniformLocation(program, "fromCrop"); p.toCrop = glGetUniformLocation(program, "toCrop"); p.fromFlip = glGetUniformLocation(program, "fromFlip"); p.toFlip = glGetUniformLocation(program, "toFlip"); glUseProgram(program); glUniform1i(GLint(p.from), 0); glUniform1i(GLint(p.to), 1); glDeleteShader(vertex); glDeleteShader(fragment); return p; } WallpaperService::TransProgram WallpaperService::programFor( const QString& file) { auto cached = m_transPrograms.constFind(file); if (cached != m_transPrograms.constEnd()) return cached.value(); TransProgram p = createTransitionProgram(file); if (p.id != 0) m_transPrograms.insert(file, p); return p; } bool WallpaperService::ensureTransitionProgram() { if (m_neutralProgram.id == 0) m_neutralProgram = programFor(QStringLiteral("wallpaper_trans_fade.frag")); return m_neutralProgram.id != 0; } QString WallpaperService::transitionFileFor( ZShell::config::WallpaperTransition::Enum value) { if (value == ZShell::config::WallpaperTransition::Random) return randomTransitionFile(); return QString(transitionFile(value)); } QString WallpaperService::randomTransitionFile() { const QStringList& files = transitionFilesList(); if (files.isEmpty()) return QStringLiteral("wallpaper_trans_fade.frag"); if (files.size() == 1) return files.at(0); if (m_transitionUses.size() != files.size()) m_transitionUses.fill(0, files.size()); double total = 0.0; QVector weights(files.size(), 0.0); for (int i = 0; i < files.size(); ++i) { if (files.at(i) == m_lastTransition) continue; weights[i] = 1.0 / double(m_transitionUses.at(i) + 1); total += weights[i]; } if (total <= 0.0) return files.at(0); double pick = QRandomGenerator::global()->generateDouble() * total; int chosen = -1; for (int i = 0; i < files.size(); ++i) { if (weights[i] <= 0.0) continue; pick -= weights[i]; if (pick <= 0.0) { chosen = i; break; } } if (chosen < 0) chosen = int(files.size()) - 1; ++m_transitionUses[chosen]; m_lastTransition = files.at(chosen); return files.at(chosen); } void WallpaperService::ensureCapture(const QSize& size) { if (m_captureFbo == 0) { glGenFramebuffers(1, &m_captureFbo); glGenTextures(1, &m_captureTex); glBindTexture(GL_TEXTURE_2D, m_captureTex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); } if (m_captureSize != size) { glBindTexture(GL_TEXTURE_2D, m_captureTex); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA8, size.width(), size.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); m_captureSize = size; } glBindFramebuffer(GL_FRAMEBUFFER, m_captureFbo); glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_captureTex, 0); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) qWarning(wpLog) << "WallpaperService: the capture framebuffer is " "incomplete"; glBindFramebuffer(GL_FRAMEBUFFER, 0); } #ifdef ZHAVE_FFMPEG 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::startDecoderVideo(const QString& path) { if (!m_decoder) m_decoder = createVideoDecoder(m_eglDisplay, backendFromConfig()); if (!m_decoder) return false; m_decoder->setReadyCallback([this]() { m_videoLoaded.store(true, std::memory_order_relaxed); m_videoReady.store(true, std::memory_order_relaxed); queueAll(); }); m_decoder->setFrameCallback([this]() { m_videoReady.store(true, std::memory_order_relaxed); queueAll(); }); if (!m_decoder->start(path)) return false; return true; } void WallpaperService::stopDecoderVideo() { if (!m_decoder) return; m_decoder->stop(); m_decoder.reset(); m_videoReady.store(false, std::memory_order_relaxed); m_videoLoaded.store(false, std::memory_order_relaxed); } #endif uint32_t WallpaperService::renderVideoIntoTexture(const QSize& size) { ensureCapture(size); if (m_captureFbo == 0 || m_captureTex == 0) return 0; glBindFramebuffer(GL_FRAMEBUFFER, m_captureFbo); glViewport(0, 0, size.width(), size.height()); glDisable(GL_BLEND); #ifdef ZHAVE_FFMPEG if (m_decoder) { if (m_decoder->textureId() != 0) { if (m_videoProgram == 0) m_videoProgram = createVideoProgram(); if (m_videoProgram != 0) { glUseProgram(m_videoProgram); const QSize src = m_decoder->frameSize(); drawVideoTexture( m_decoder->textureId(), m_decoder->chromaTextureId(), m_decoder->colorMatrix(), m_decoder->colorRange(), QRectF(0.0, 0.0, 1.0, 1.0), float(size.width()), float(size.height()), src.width() > 0 ? float(src.width()) : float(size.width()), src.height() > 0 ? float(src.height()) : float(size.height()), 1.0f); } } } else #endif { if (!m_mpvRender) initMpvRender(); if (!m_mpvRender) { glBindFramebuffer(GL_FRAMEBUFFER, 0); return m_captureTex; } glViewport(0, 0, size.width(), size.height()); mpv_opengl_fbo fbo{ int(m_captureFbo), size.width(), size.height(), GL_RGBA}; int flip = 1; int block = 0; mpv_render_param params[]{ {MPV_RENDER_PARAM_OPENGL_FBO, &fbo}, {MPV_RENDER_PARAM_FLIP_Y, &flip}, {MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME, &block}, {MPV_RENDER_PARAM_INVALID, nullptr}, }; mpv_render_context_render(m_mpvRender, params); } glBindFramebuffer(GL_FRAMEBUFFER, 0); return m_captureTex; } void WallpaperService::uploadTexture(uint32_t& texture, const QImage& image) { if (image.isNull()) return; if (texture == 0) { glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); } else { glBindTexture(GL_TEXTURE_2D, texture); } glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA8, image.width(), image.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, image.constBits()); } void WallpaperService::uploadImage() { uploadTexture(m_texture, m_image); } void WallpaperService::drawTexture( uint32_t texture, const QRectF& rect, float sizeW, float sizeH, float alpha) { glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture); glUniform1i(GLint(m_sourceUniform), 0); glUniform2f(GLint(m_sizeUniform), sizeW, sizeH); glUniform4f( GLint(m_cropUniform), GLfloat(rect.x()), GLfloat(rect.y()), GLfloat(rect.width()), GLfloat(rect.height())); glUniform1f(GLint(m_alphaUniform), alpha); glDrawArrays(GL_TRIANGLES, 0, 3); } 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 = m_cache.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::ensurePoster() { QString path; { std::lock_guard lock(m_mutex); if (!m_isVideo) return; path = m_path; if (m_posterPath == path && (m_posterReady || m_posterLoading)) return; if (m_posterLoading) return; m_posterPath = path; m_posterReady = false; m_posterLoading = true; m_posterImage = QImage(); } QThreadPool::globalInstance()->start([this, path]() { const QByteArray blob = m_cache.posterBlob(path); QImage image; if (!blob.isEmpty()) { image = QImage::fromData(blob, "JPEG") .convertToFormat(QImage::Format_RGBA8888); } std::lock_guard lock(m_mutex); m_posterImage = image; m_posterReady = !image.isNull(); m_posterChanged = m_posterReady; m_posterLoading = false; if (!m_posterReady) qWarning(wpLog) << "WallpaperService: poster decode failed for" << path; }); wake(); } void WallpaperService::invalidateCache() { { std::lock_guard 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 (!m_transitions.empty()) { drawSurfaceTransition(surface); return; } if (!surface->needsDraw() || !surface->frameReady()) return; EGLSurface target = surface->eglSurface(); if (m_context == EGL_NO_CONTEXT || target == EGL_NO_SURFACE || surface->bufferSize().isEmpty()) return; if (!eglMakeCurrent(m_eglDisplay, target, target, m_context)) { qWarning(wpLog) << "WallpaperService: the context cannot be made " "current, eglGetError = 0x" << Qt::hex << unsigned(eglGetError()); return; } eglSwapInterval(m_eglDisplay, 0); if (m_vao == 0) { glGenVertexArrays(1, &m_vao); glBindVertexArray(m_vao); } struct ContextRelease { EGLDisplay display; ~ContextRelease() { eglMakeCurrent( display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); } } contextRelease{m_eglDisplay}; QVariantMap crops; QSize size; { std::lock_guard 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_FFMPEG const bool videoAsBackground = videoNow && (m_mpv != nullptr || m_decoder != nullptr); #else const bool videoAsBackground = videoNow && m_mpv != nullptr; #endif if (videoAsBackground) { glDisable(GL_BLEND); glViewport(0, 0, size.width(), size.height()); #ifdef ZHAVE_FFMPEG if (m_decoder) { if (m_decoder->textureId() != 0) { if (m_videoProgram == 0) m_videoProgram = createVideoProgram(); if (m_videoProgram != 0) { glUseProgram(m_videoProgram); const QSize src = m_decoder->frameSize(); drawVideoTexture( m_decoder->textureId(), m_decoder->chromaTextureId(), m_decoder->colorMatrix(), m_decoder->colorRange(), QRectF(0.0, 0.0, 1.0, 1.0), float(size.width()), float(size.height()), src.width() > 0 ? float(src.width()) : float(size.width()), src.height() > 0 ? float(src.height()) : float(size.height()), 1.0f); } } } else #endif { if (!m_mpvRender) initMpvRender(); if (!m_mpvRender) return; renderMpvToSurface(size); } } else { glUseProgram(m_program); glViewport(0, 0, size.width(), size.height()); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); } if (!videoNow) { const bool upload = m_imageChanged; m_imageChanged = false; if (m_texture == 0 || upload) uploadImage(); if (m_texture == 0 || m_image.isNull()) return; } glUseProgram(m_program); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); const float w = float(size.width()); const float h = float(size.height()); if (!videoNow) { const QRectF rect = sourceRect(crops, surface->screen(), size, m_image.size()); drawTexture(m_texture, rect, w, h, 1.0f); } } surface->requestFrame(); if (!eglSwapBuffers(m_eglDisplay, target)) { qWarning(wpLog) << "WallpaperService: eglSwapBuffers failed, eglGetError = 0x" << QString::number(unsigned(eglGetError()), 16); return; } if (m_isVideo && m_mpvRender) mpv_render_context_report_swap(m_mpvRender); surface->endDraw(); } void WallpaperService::ensureBaseFbo() { if (m_baseFbo != 0) return; glGenFramebuffers(1, &m_baseFbo); glGenTextures(1, &m_baseTex); glBindTexture(GL_TEXTURE_2D, m_baseTex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); } void WallpaperService::ensureAccum(const QSize& size) { if (m_accumFbo == 0) { glGenFramebuffers(1, &m_accumFbo); glGenTextures(1, &m_accumA); glGenTextures(1, &m_accumB); for (uint32_t tex : {m_accumA, m_accumB}) { glBindTexture(GL_TEXTURE_2D, tex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); } } if (m_accumSize != size) { for (uint32_t tex : {m_accumA, m_accumB}) { glBindTexture(GL_TEXTURE_2D, tex); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA8, size.width(), size.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); } m_accumSize = size; } } void WallpaperService::bindRtt(uint32_t fbo, uint32_t tex, const QSize& size) { glBindFramebuffer(GL_FRAMEBUFFER, fbo); glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); glViewport(0, 0, size.width(), size.height()); } void WallpaperService::drawTransitionPair( const TransProgram& program, uint32_t fromTex, float fromFlip, const QRectF& fromCrop, uint32_t toTex, float toFlip, const QRectF& toCrop, qreal progress, const QSize& size) { glUseProgram(program.id); glDisable(GL_BLEND); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, fromTex); glUniform1i(GLint(program.from), 0); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, toTex); glUniform1i(GLint(program.to), 1); glUniform2f(GLint(program.size), float(size.width()), float(size.height())); glUniform4f( GLint(program.fromCrop), GLfloat(fromCrop.x()), GLfloat(fromCrop.y()), GLfloat(fromCrop.width()), GLfloat(fromCrop.height())); glUniform4f( GLint(program.toCrop), GLfloat(toCrop.x()), GLfloat(toCrop.y()), GLfloat(toCrop.width()), GLfloat(toCrop.height())); glUniform1f(GLint(program.fromFlip), fromFlip); glUniform1f(GLint(program.toFlip), toFlip); glUniform1f(GLint(program.progress), float(progress)); glDrawArrays(GL_TRIANGLES, 0, 3); } uint32_t WallpaperService::targetTexFor( const TransitionLayer& layer, int index) const { if (layer.isVideo) return m_captureTex; if (layer.tex != 0) return layer.tex; Q_UNUSED(index); return m_texture; } QRectF WallpaperService::cropForLayer( const TransitionLayer& layer, const QString& screen, const QSize& size) const { if (layer.isVideo) return QRectF(0.0, 0.0, 1.0, 1.0); if (layer.tex != 0) return sourceRect(layer.crops, screen, size, layer.image.size()); return sourceRect(m_crops, screen, size, m_image.size()); } void WallpaperService::captureBase( const QSize& size, const QString& screen, uint32_t oldTex, float oldFlip, const QRectF& oldCrop) { Q_UNUSED(screen); ensureBaseFbo(); if (m_baseTex == 0) return; Q_UNUSED(oldCrop); if (!m_baseImage.isNull()) { uploadTexture(m_baseTex, m_baseImage); m_baseSize = m_baseImage.size(); m_baseReady = true; return; } if (m_baseSize != size) { glBindTexture(GL_TEXTURE_2D, m_baseTex); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA8, size.width(), size.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); m_baseSize = size; } bindRtt(m_baseFbo, m_baseTex, size); drawTransitionPair( m_neutralProgram, oldTex, oldFlip, QRectF(0.0, 0.0, 1.0, 1.0), oldTex, oldFlip, QRectF(0.0, 0.0, 1.0, 1.0), 1.0, size); glBindFramebuffer(GL_FRAMEBUFFER, 0); m_baseReady = true; } void WallpaperService::renderComposite( const QSize& size, const QString& screen, uint32_t outFbo, uint32_t outTex, int first, int last, uint32_t videoTex, uint32_t posterTex) { if (m_baseTex == 0) return; ensureAccum(size); float fromFlip = 0.0f; QRectF fromCrop = QRectF(0.0, 0.0, 1.0, 1.0); if (!m_baseImage.isNull()) { fromFlip = 1.0f; fromCrop = sourceRect(m_baseCrops, screen, size, m_baseImage.size()); } bindRtt(m_accumFbo, m_accumA, size); drawTransitionPair( m_neutralProgram, m_baseTex, fromFlip, fromCrop, m_baseTex, fromFlip, fromCrop, 1.0, size); uint32_t read = m_accumA; uint32_t write = m_accumB; int drawn = 0; for (int i = first; i < last; ++i) { const TransitionLayer& layer = m_transitions[size_t(i)]; if (!layer.ready) continue; uint32_t toTex = 0; float toFlip = 1.0f; QRectF toCrop(0.0, 0.0, 1.0, 1.0); if (layer.isVideo) { if (m_videoReady.load(std::memory_order_relaxed) && videoTex != 0) { toTex = videoTex; toFlip = 0.0f; } else if (posterTex != 0) { toTex = posterTex; toFlip = 1.0f; toCrop = sourceRect(layer.crops, screen, size, m_posterImage.size()); } else { toTex = videoTex; toFlip = 0.0f; } } else { toTex = targetTexFor(layer, i); toCrop = cropForLayer(layer, screen, size); } if (toTex == 0) continue; const TransProgram program = layer.transitionFile.isEmpty() ? m_neutralProgram : programFor(layer.transitionFile); bindRtt(m_accumFbo, write, size); drawTransitionPair( program, read, 0.0f, QRectF(0.0, 0.0, 1.0, 1.0), toTex, toFlip, toCrop, layer.progress, size); std::swap(read, write); ++drawn; } if (drawn == 0) { if (outFbo == m_baseFbo) return; read = m_accumA; } if (outFbo == 0) { glBindFramebuffer(GL_FRAMEBUFFER, 0); glViewport(0, 0, size.width(), size.height()); } else { bindRtt(outFbo, outTex, size); } drawTransitionPair( m_neutralProgram, read, 0.0f, QRectF(0.0, 0.0, 1.0, 1.0), read, 0.0f, QRectF(0.0, 0.0, 1.0, 1.0), 1.0, size); } void WallpaperService::drawSurfaceTransition(WallpaperSurface* surface) { if (!surface->needsDraw() || !surface->frameReady()) return; EGLSurface target = surface->eglSurface(); if (m_context == EGL_NO_CONTEXT || target == EGL_NO_SURFACE || surface->bufferSize().isEmpty()) return; if (!eglMakeCurrent(m_eglDisplay, target, target, m_context)) { qWarning(wpLog) << "WallpaperService: the context cannot be made " "current, eglGetError = 0x" << Qt::hex << unsigned(eglGetError()); return; } eglSwapInterval(m_eglDisplay, 0); if (m_vao == 0) { glGenVertexArrays(1, &m_vao); glBindVertexArray(m_vao); } struct ContextRelease { EGLDisplay display; ~ContextRelease() { eglMakeCurrent( display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); } } contextRelease{m_eglDisplay}; { std::lock_guard lock(m_mutex); const QSize size = surface->bufferSize(); if (size.isEmpty()) return; const QString screen = surface->screen(); if (m_transitions.empty()) return; if (!ensureTransitionProgram()) return; if (m_blackTex == 0) createBlackTexture(m_blackTex); ensureBaseFbo(); const bool newestVideoLive = !m_transitions.empty() && m_transitions.back().isVideo && m_videoReady.load(std::memory_order_relaxed); bool anyVideoLayer = false; for (const auto& layer : m_transitions) if (layer.isVideo) { anyVideoLayer = true; break; } bool needVideo = (m_needCaptureBase && m_baseIsVideo) || (m_videoReady.load(std::memory_order_relaxed) && anyVideoLayer); const uint32_t videoTex = needVideo ? renderVideoIntoTexture(size) : 0; if (m_needCaptureBase && m_baseIsVideo && videoTex == 0) return; if (m_needCaptureBase) { uint32_t oldTex = 0; float oldFlip = 0.0f; QRectF oldCrop(0.0, 0.0, 1.0, 1.0); if (m_baseIsBlack) { oldTex = m_blackTex; oldFlip = 1.0f; } else if (m_baseIsVideo) { oldTex = videoTex; oldFlip = 0.0f; } else { oldTex = m_texture; oldFlip = 1.0f; oldCrop = sourceRect(m_baseCrops, screen, size, m_baseImage.size()); } if (oldTex != 0) captureBase(size, screen, oldTex, oldFlip, oldCrop); } if (!m_transitions.back().isVideo) { const bool upload = m_imageChanged; m_imageChanged = false; if (m_texture == 0 || upload) uploadImage(); } else { m_imageChanged = false; } if (m_isVideo && m_posterChanged) { m_posterChanged = false; if (!m_posterImage.isNull()) uploadTexture(m_posterTex, m_posterImage); } const bool newestVideoPoster = newestVideoLive == false && m_transitions.back().isVideo && m_posterReady && m_posterTex != 0; const bool newestReady = m_isVideo ? (newestVideoLive || newestVideoPoster) : (!m_image.isNull() && m_texture != 0 && m_replacementReady); for (size_t i = 0; i < m_transitions.size(); ++i) { TransitionLayer& layer = m_transitions[i]; const bool ready = (i + 1 == m_transitions.size()) ? newestReady : (layer.isVideo ? (layer.ready || m_videoReady || m_posterReady) : layer.tex != 0); if (!ready) continue; layer.ready = true; if (!layer.started) { layer.started = true; layer.timer.start(); } layer.progress = qreal(layer.timer.elapsed()) / qreal(kFadeDurationMs); if (layer.progress > 1.0) layer.progress = 1.0; } const bool newestSettled = m_transitions.back().isVideo ? newestVideoLive : true; const bool collapse = m_collapseVideo && m_transitions.size() > 1; const bool promote = m_promoteOldest && !m_transitions.empty(); if (collapse) { ensureAccum(size); renderComposite( size, screen, m_baseFbo, m_baseTex, 0, int(m_transitions.size()) - 1, videoTex, m_posterTex); } else if (promote) { ensureAccum(size); renderComposite( size, screen, m_baseFbo, m_baseTex, 0, 1, videoTex, m_posterTex); } int first = 0; if (collapse) first = int(m_transitions.size()) - 1; else if (promote) first = 1; const int last = int(m_transitions.size()); const TransitionLayer& newest = m_transitions.back(); if (m_baseIsBlack) { const qreal p = newest.ready ? newest.progress : 0.0; const float a = p < 0.0 ? 0.0f : (p > 1.0 ? 1.0f : float(p)); const float w = float(size.width()); const float h = float(size.height()); if (m_program == 0) m_program = createProgram(); if (m_program == 0) return; glBindFramebuffer(GL_FRAMEBUFFER, 0); glViewport(0, 0, size.width(), size.height()); if (m_isVideo) { if (!m_videoReady.load(std::memory_order_relaxed)) { if (!m_posterReady || m_posterTex == 0) return; glUseProgram(m_program); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); drawTexture( m_posterTex, sourceRect(m_crops, screen, size, m_posterImage.size()), w, h, a); } else { glDisable(GL_BLEND); #ifdef ZHAVE_FFMPEG if (m_decoder) { if (m_decoder->textureId() != 0) { if (m_videoProgram == 0) m_videoProgram = createVideoProgram(); if (m_videoProgram != 0) { glUseProgram(m_videoProgram); const QSize src = m_decoder->frameSize(); drawVideoTexture( m_decoder->textureId(), m_decoder->chromaTextureId(), m_decoder->colorMatrix(), m_decoder->colorRange(), QRectF(0.0, 0.0, 1.0, 1.0), w, h, src.width() > 0 ? float(src.width()) : w, src.height() > 0 ? float(src.height()) : h, 1.0f); } } } else #endif { if (!m_mpvRender) initMpvRender(); if (!m_mpvRender) return; renderMpvToSurface(size); } glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glUseProgram(m_program); if (m_blackTex != 0) drawTexture( m_blackTex, QRectF(0.0, 0.0, 1.0, 1.0), w, h, 1.0f - a); } } else { if (m_texture == 0 || m_image.isNull()) return; glUseProgram(m_program); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); drawTexture( m_texture, sourceRect(m_crops, screen, size, m_image.size()), w, h, a); } if (newest.ready && newest.progress >= 1.0 && newestSettled) m_newestDone = true; } else { glBindFramebuffer(GL_FRAMEBUFFER, 0); glViewport(0, 0, size.width(), size.height()); if (!m_baseReady) { glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); } else { renderComposite( size, screen, 0, 0, first, last, videoTex, m_posterTex); } if (newest.ready && newest.progress >= 1.0 && newestSettled) m_newestDone = true; } } surface->requestFrame(); if (!eglSwapBuffers(m_eglDisplay, target)) { qWarning(wpLog) << "WallpaperService: eglSwapBuffers failed, eglGetError = 0x" << QString::number(unsigned(eglGetError()), 16); return; } if ((m_isVideo || m_baseIsVideo) && m_mpvRender) mpv_render_context_report_swap(m_mpvRender); surface->endDraw(); } void WallpaperService::addSurface(const QString& screen) { if (!m_layerShell || !m_viewporter || !m_scaleManager) return; QScreen* qscreen = nullptr; for (auto* candidate : QGuiApplication::screens()) { if (candidate->name() == screen) { qscreen = candidate; break; } } if (!qscreen) { qWarning(wpLog) << "WallpaperService:" << screen << "is not a screen"; return; } auto* waylandScreen = dynamic_cast(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::wallpaper