From b0cb43f75c61bedd38c762d9b74c05a81d1b086d Mon Sep 17 00:00:00 2001 From: zach Date: Tue, 27 Oct 2026 02:15:16 +0100 Subject: [PATCH] feat: add vaapi path for video wallpapers on generic gpus --- -a | 3 + Helpers/Wallpapers.qml | 2 +- Plugins/CMakeLists.txt | 1 + Plugins/ZShell/CMakeLists.txt | 1 + Plugins/ZShell/Internal/CMakeLists.txt | 41 -- Plugins/ZShell/Services/dropimporter.cpp | 2 +- Plugins/ZShell/Wallpaper/CMakeLists.txt | 70 ++ .../nvidiaplayer.cpp} | 120 ++-- .../nvidiaplayer.hpp} | 48 +- .../protocols/wlr-layer-shell-unstable-v1.xml | 0 .../shaders/wallpaper.frag | 0 .../shaders/wallpaper.vert | 0 .../shaders/wallpaper_trans_bowtie.frag | 0 .../shaders/wallpaper_trans_doom_melt.frag | 0 .../shaders/wallpaper_trans_fade.frag | 0 .../shaders/wallpaper_trans_hexagonalize.frag | 0 .../shaders/wallpaper_trans_linear_blur.frag | 0 ...paper_trans_split_slide_in_horizontal.frag | 0 .../shaders/wallpaper_trans_tiles_wave.frag | 0 .../wallpaper_trans_window_slices.frag | 0 .../shaders/wallpaper_trans_wipe_down.frag | 0 .../shaders/wallpaper_trans_wipe_right.frag | 0 .../shaders/wallpaper_video.frag | 0 Plugins/ZShell/Wallpaper/vaapiplayer.cpp | 628 ++++++++++++++++++ Plugins/ZShell/Wallpaper/vaapiplayer.hpp | 83 +++ Plugins/ZShell/Wallpaper/videodecoder.cpp | 117 ++++ Plugins/ZShell/Wallpaper/videodecoder.hpp | 58 ++ Plugins/ZShell/Wallpaper/videotranscoder.cpp | 165 +++++ Plugins/ZShell/Wallpaper/videotranscoder.hpp | 33 + Plugins/ZShell/Wallpaper/wallpapercache.cpp | 170 +++++ Plugins/ZShell/Wallpaper/wallpapercache.hpp | 33 + .../wallpaperimage.cpp | 4 +- .../wallpaperimage.hpp | 4 +- .../wallpaperobject.cpp | 4 +- .../wallpaperobject.hpp | 4 +- .../wallpaperservice.cpp | 579 +++++----------- .../wallpaperservice.hpp | 91 ++- .../wallpapersurface.cpp | 4 +- .../wallpapersurface.hpp | 4 +- 39 files changed, 1670 insertions(+), 599 deletions(-) create mode 100644 -a create mode 100644 Plugins/ZShell/Wallpaper/CMakeLists.txt rename Plugins/ZShell/{Internal/videoplayer.cpp => Wallpaper/nvidiaplayer.cpp} (89%) rename Plugins/ZShell/{Internal/videoplayer.hpp => Wallpaper/nvidiaplayer.hpp} (59%) rename Plugins/ZShell/{Internal => Wallpaper}/protocols/wlr-layer-shell-unstable-v1.xml (100%) rename Plugins/ZShell/{Internal => Wallpaper}/shaders/wallpaper.frag (100%) rename Plugins/ZShell/{Internal => Wallpaper}/shaders/wallpaper.vert (100%) rename Plugins/ZShell/{Internal => Wallpaper}/shaders/wallpaper_trans_bowtie.frag (100%) rename Plugins/ZShell/{Internal => Wallpaper}/shaders/wallpaper_trans_doom_melt.frag (100%) rename Plugins/ZShell/{Internal => Wallpaper}/shaders/wallpaper_trans_fade.frag (100%) rename Plugins/ZShell/{Internal => Wallpaper}/shaders/wallpaper_trans_hexagonalize.frag (100%) rename Plugins/ZShell/{Internal => Wallpaper}/shaders/wallpaper_trans_linear_blur.frag (100%) rename Plugins/ZShell/{Internal => Wallpaper}/shaders/wallpaper_trans_split_slide_in_horizontal.frag (100%) rename Plugins/ZShell/{Internal => Wallpaper}/shaders/wallpaper_trans_tiles_wave.frag (100%) rename Plugins/ZShell/{Internal => Wallpaper}/shaders/wallpaper_trans_window_slices.frag (100%) rename Plugins/ZShell/{Internal => Wallpaper}/shaders/wallpaper_trans_wipe_down.frag (100%) rename Plugins/ZShell/{Internal => Wallpaper}/shaders/wallpaper_trans_wipe_right.frag (100%) rename Plugins/ZShell/{Internal => Wallpaper}/shaders/wallpaper_video.frag (100%) create mode 100644 Plugins/ZShell/Wallpaper/vaapiplayer.cpp create mode 100644 Plugins/ZShell/Wallpaper/vaapiplayer.hpp create mode 100644 Plugins/ZShell/Wallpaper/videodecoder.cpp create mode 100644 Plugins/ZShell/Wallpaper/videodecoder.hpp create mode 100644 Plugins/ZShell/Wallpaper/videotranscoder.cpp create mode 100644 Plugins/ZShell/Wallpaper/videotranscoder.hpp create mode 100644 Plugins/ZShell/Wallpaper/wallpapercache.cpp create mode 100644 Plugins/ZShell/Wallpaper/wallpapercache.hpp rename Plugins/ZShell/{Internal => Wallpaper}/wallpaperimage.cpp (99%) rename Plugins/ZShell/{Internal => Wallpaper}/wallpaperimage.hpp (98%) rename Plugins/ZShell/{Internal => Wallpaper}/wallpaperobject.cpp (97%) rename Plugins/ZShell/{Internal => Wallpaper}/wallpaperobject.hpp (96%) rename Plugins/ZShell/{Internal => Wallpaper}/wallpaperservice.cpp (84%) rename Plugins/ZShell/{Internal => Wallpaper}/wallpaperservice.hpp (75%) rename Plugins/ZShell/{Internal => Wallpaper}/wallpapersurface.cpp (98%) rename Plugins/ZShell/{Internal => Wallpaper}/wallpapersurface.hpp (96%) diff --git a/-a b/-a new file mode 100644 index 0000000..34b9e9d --- /dev/null +++ b/-a @@ -0,0 +1,3 @@ + +#omz revision: d2b7f557cceaddb16ba89bbf8200d8a8df35c4d8 +#omz fpath: /home/zach/.oh-my-zsh/plugins/colored-man-pages /home/zach/.oh-my-zsh/plugins/universalarchive /home/zach/.oh-my-zsh/plugins/tldr /home/zach/.oh-my-zsh/plugins/extract /home/zach/.oh-my-zsh/plugins/copypath /home/zach/.oh-my-zsh/plugins/cp /home/zach/.oh-my-zsh/plugins/conda-env /home/zach/.oh-my-zsh/plugins/command-not-found /home/zach/.oh-my-zsh/plugins/dirhistory /home/zach/.oh-my-zsh/plugins/copybuffer /home/zach/.oh-my-zsh/plugins/copyfile /home/zach/.oh-my-zsh/custom/plugins/fast-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-autosuggestions /home/zach/.oh-my-zsh/plugins/archlinux /home/zach/.oh-my-zsh/plugins/web-search /home/zach/.oh-my-zsh/plugins/sudo /home/zach/.oh-my-zsh/plugins/git /home/zach/.oh-my-zsh/functions /home/zach/.oh-my-zsh/completions /home/zach/.oh-my-zsh/custom/functions /home/zach/.oh-my-zsh/custom/completions /home/zach/tools/ripgrep/complete /home/linuxbrew/.linuxbrew/share/zsh/site-functions /home/zach/.oh-my-zsh/plugins/colored-man-pages /home/zach/.oh-my-zsh/plugins/universalarchive /home/zach/.oh-my-zsh/plugins/tldr /home/zach/.oh-my-zsh/plugins/extract /home/zach/.oh-my-zsh/plugins/copypath /home/zach/.oh-my-zsh/plugins/cp /home/zach/.oh-my-zsh/plugins/conda-env /home/zach/.oh-my-zsh/plugins/command-not-found /home/zach/.oh-my-zsh/plugins/dirhistory /home/zach/.oh-my-zsh/plugins/copybuffer /home/zach/.oh-my-zsh/plugins/copyfile /home/zach/.oh-my-zsh/custom/plugins/fast-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-autosuggestions /home/zach/.oh-my-zsh/plugins/archlinux /home/zach/.oh-my-zsh/plugins/web-search /home/zach/.oh-my-zsh/plugins/sudo /home/zach/.oh-my-zsh/plugins/git /home/zach/.oh-my-zsh/functions /home/zach/.oh-my-zsh/completions /home/zach/.oh-my-zsh/custom/functions /home/zach/.oh-my-zsh/custom/completions /home/zach/.oh-my-zsh/cache/completions /home/zach/tools/ripgrep/complete /home/linuxbrew/.linuxbrew/share/zsh/site-functions /usr/local/share/zsh/site-functions /usr/share/zsh/site-functions /usr/share/zsh/functions/Calendar /usr/share/zsh/functions/Chpwd /usr/share/zsh/functions/Completion /usr/share/zsh/functions/Completion/Base /usr/share/zsh/functions/Completion/Linux /usr/share/zsh/functions/Completion/Unix /usr/share/zsh/functions/Completion/X /usr/share/zsh/functions/Completion/Zsh /usr/share/zsh/functions/Exceptions /usr/share/zsh/functions/MIME /usr/share/zsh/functions/Math /usr/share/zsh/functions/Misc /usr/share/zsh/functions/Newuser /usr/share/zsh/functions/Prompts /usr/share/zsh/functions/TCP /usr/share/zsh/functions/VCS_Info /usr/share/zsh/functions/VCS_Info/Backends /usr/share/zsh/functions/Zftp /usr/share/zsh/functions/Zle /home/zach/.zfunc diff --git a/Helpers/Wallpapers.qml b/Helpers/Wallpapers.qml index e8226ae..9a0717e 100644 --- a/Helpers/Wallpapers.qml +++ b/Helpers/Wallpapers.qml @@ -5,7 +5,7 @@ import Quickshell import Quickshell.Io import QtQuick import ZShell.Models -import ZShell.Internal +import ZShell.Wallpaper import ZShell.Config import qs.Components import qs.Services diff --git a/Plugins/CMakeLists.txt b/Plugins/CMakeLists.txt index 4e87c55..8cc48df 100644 --- a/Plugins/CMakeLists.txt +++ b/Plugins/CMakeLists.txt @@ -25,6 +25,7 @@ pkg_check_modules(MPV REQUIRED IMPORTED_TARGET mpv) pkg_check_modules(AVCODEC REQUIRED IMPORTED_TARGET libavcodec) pkg_check_modules(AVFORMAT REQUIRED IMPORTED_TARGET libavformat) pkg_check_modules(AVUTIL REQUIRED IMPORTED_TARGET libavutil) +pkg_check_modules(LIBVA QUIET IMPORTED_TARGET libva) find_package(CUDAToolkit QUIET) pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) diff --git a/Plugins/ZShell/CMakeLists.txt b/Plugins/ZShell/CMakeLists.txt index 7d1bbaf..7d69f21 100644 --- a/Plugins/ZShell/CMakeLists.txt +++ b/Plugins/ZShell/CMakeLists.txt @@ -37,6 +37,7 @@ list(APPEND CMAKE_AUTOMOC_MACRO_NAMES add_subdirectory(Settings) add_subdirectory(Models) add_subdirectory(Internal) +add_subdirectory(Wallpaper) add_subdirectory(Services) add_subdirectory(Components) add_subdirectory(Blobs) diff --git a/Plugins/ZShell/Internal/CMakeLists.txt b/Plugins/ZShell/Internal/CMakeLists.txt index 482460d..ff6b832 100644 --- a/Plugins/ZShell/Internal/CMakeLists.txt +++ b/Plugins/ZShell/Internal/CMakeLists.txt @@ -1,8 +1,3 @@ -wl_proto(wlp-layer-shell wlr-layer-shell-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}/protocols") -wl_proto(wlp-viewporter viewporter "${WAYLAND_PROTOCOLS}/stable/viewporter") -wl_proto(wlp-fractional-scale fractional-scale-v1 "${WAYLAND_PROTOCOLS}/staging/fractional-scale") -wl_proto(wlp-xdg-shell xdg-shell "${WAYLAND_PROTOCOLS}/stable/xdg-shell") - qml_module(ZShell-internal URI ZShell.Internal INCLUDE_PREFIX internal @@ -13,16 +8,11 @@ qml_module(ZShell-internal circularindicatormanager.hpp circularindicatormanager.cpp circularbuffer.hpp circularbuffer.cpp arcgauge.hpp arcgauge.cpp - wallpaperimage.hpp wallpaperimage.cpp - wallpaperservice.hpp wallpaperservice.cpp - wallpapersurface.hpp wallpapersurface.cpp - wallpaperobject.hpp wallpaperobject.cpp lidwatcher.hpp lidwatcher.cpp visualizerbars.hpp visualizerbars.cpp linearindicatormanager.hpp linearindicatormanager.cpp strokecanvasitem.hpp strokecanvasitem.cpp strokecanvasrenderer.hpp strokecanvasrenderer.cpp - videoplayer.hpp videoplayer.cpp cursor.hpp LIBRARIES Qt::Gui @@ -34,39 +24,8 @@ qml_module(ZShell-internal Qt::DBus Qt::CanvasPainter Qt::GuiPrivate - wlp-layer-shell - wlp-xdg-shell - wlp-viewporter - wlp-fractional-scale PkgConfig::EGL PkgConfig::WAYLAND_EGL PkgConfig::GL - PkgConfig::MPV - PkgConfig::AVCODEC - PkgConfig::AVFORMAT - PkgConfig::AVUTIL ZShell-config ) - -qt_add_resources(ZShell-internal "wallpaper_shaders" - PREFIX "/" - FILES - shaders/wallpaper.vert - shaders/wallpaper.frag - shaders/wallpaper_video.frag - shaders/wallpaper_trans_bowtie.frag - shaders/wallpaper_trans_fade.frag - shaders/wallpaper_trans_linear_blur.frag - shaders/wallpaper_trans_window_slices.frag - shaders/wallpaper_trans_wipe_right.frag - shaders/wallpaper_trans_wipe_down.frag - shaders/wallpaper_trans_tiles_wave.frag - shaders/wallpaper_trans_split_slide_in_horizontal.frag - shaders/wallpaper_trans_hexagonalize.frag - shaders/wallpaper_trans_doom_melt.frag -) - -if(CUDAToolkit_FOUND) - target_link_libraries(ZShell-internal PRIVATE CUDA::cuda_driver) - target_compile_definitions(ZShell-internal PRIVATE ZHAVE_VIDEO=1) -endif() diff --git a/Plugins/ZShell/Services/dropimporter.cpp b/Plugins/ZShell/Services/dropimporter.cpp index 167e941..3cac93f 100644 --- a/Plugins/ZShell/Services/dropimporter.cpp +++ b/Plugins/ZShell/Services/dropimporter.cpp @@ -93,7 +93,7 @@ int DropImporter::importAttachments( saveBytes(id, rawImage); } else if (const QString src = firstImgSrc(html); !src.isEmpty()) { if (src.startsWith(QLatin1String("data:"), Qt::CaseInsensitive)) { - const int comma = src.indexOf(QLatin1Char(',')); + const qsizetype comma = src.indexOf(QLatin1Char(',')); if (comma < 0) { failWith(id, tr("Malformed data URI")); } else { diff --git a/Plugins/ZShell/Wallpaper/CMakeLists.txt b/Plugins/ZShell/Wallpaper/CMakeLists.txt new file mode 100644 index 0000000..2a3186a --- /dev/null +++ b/Plugins/ZShell/Wallpaper/CMakeLists.txt @@ -0,0 +1,70 @@ +wl_proto(wlp-layer-shell wlr-layer-shell-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}/protocols") +wl_proto(wlp-viewporter viewporter "${WAYLAND_PROTOCOLS}/stable/viewporter") +wl_proto(wlp-fractional-scale fractional-scale-v1 "${WAYLAND_PROTOCOLS}/staging/fractional-scale") +wl_proto(wlp-xdg-shell xdg-shell "${WAYLAND_PROTOCOLS}/stable/xdg-shell") + +qml_module(ZShell-Wallpaper + URI ZShell.Wallpaper + INCLUDE_PREFIX wallpaper + SOURCES + wallpaperservice.hpp wallpaperservice.cpp + wallpaperobject.hpp wallpaperobject.cpp + wallpapersurface.hpp wallpapersurface.cpp + wallpaperimage.hpp wallpaperimage.cpp + wallpapercache.hpp wallpapercache.cpp + videotranscoder.hpp videotranscoder.cpp + videodecoder.hpp videodecoder.cpp + nvidiaplayer.hpp nvidiaplayer.cpp + vaapiplayer.hpp vaapiplayer.cpp + LIBRARIES + Qt::Gui + Qt::Quick + Qt::Concurrent + Qt::Sql + Qt::Core + Qt::GuiPrivate + Qt::WaylandClient + Qt::WaylandClientPrivate + wlp-layer-shell + wlp-xdg-shell + wlp-viewporter + wlp-fractional-scale + PkgConfig::EGL + PkgConfig::WAYLAND_EGL + PkgConfig::GL + PkgConfig::MPV + PkgConfig::AVCODEC + PkgConfig::AVFORMAT + PkgConfig::AVUTIL + ZShell-config +) + +target_compile_definitions(ZShell-Wallpaper PRIVATE ZHAVE_FFMPEG=1) + +if(LIBVA_FOUND) + target_link_libraries(ZShell-Wallpaper PRIVATE PkgConfig::LIBVA) + target_compile_definitions(ZShell-Wallpaper PRIVATE ZHAVE_VAAPI=1) +endif() + +if(CUDAToolkit_FOUND) + target_link_libraries(ZShell-Wallpaper PRIVATE CUDA::cuda_driver) + target_compile_definitions(ZShell-Wallpaper PRIVATE ZHAVE_VIDEO=1) +endif() + +qt_add_resources(ZShell-Wallpaper "wallpaper_shaders" + PREFIX "/" + FILES + shaders/wallpaper.vert + shaders/wallpaper.frag + shaders/wallpaper_video.frag + shaders/wallpaper_trans_bowtie.frag + shaders/wallpaper_trans_fade.frag + shaders/wallpaper_trans_linear_blur.frag + shaders/wallpaper_trans_window_slices.frag + shaders/wallpaper_trans_wipe_right.frag + shaders/wallpaper_trans_wipe_down.frag + shaders/wallpaper_trans_tiles_wave.frag + shaders/wallpaper_trans_split_slide_in_horizontal.frag + shaders/wallpaper_trans_hexagonalize.frag + shaders/wallpaper_trans_doom_melt.frag +) diff --git a/Plugins/ZShell/Internal/videoplayer.cpp b/Plugins/ZShell/Wallpaper/nvidiaplayer.cpp similarity index 89% rename from Plugins/ZShell/Internal/videoplayer.cpp rename to Plugins/ZShell/Wallpaper/nvidiaplayer.cpp index ed14b78..1a2463c 100644 --- a/Plugins/ZShell/Internal/videoplayer.cpp +++ b/Plugins/ZShell/Wallpaper/nvidiaplayer.cpp @@ -1,7 +1,7 @@ -#include "videoplayer.hpp" - #ifdef ZHAVE_VIDEO +#include "nvidiaplayer.hpp" + #define EGL_EGLEXT_PROTOTYPES #define GL_GLEXT_PROTOTYPES @@ -24,6 +24,8 @@ extern "C" CUresult cuGraphicsGLRegisterImage( Q_LOGGING_CATEGORY(videoLog, "ZShell.Internal.wallpaper") +namespace ZShell::wallpaper { + namespace { enum AVPixelFormat chooseFormat( @@ -34,14 +36,34 @@ enum AVPixelFormat chooseFormat( return pix_fmts[0]; } +class CudaCtx { + public: + explicit CudaCtx(CUcontext ctx) : m_ctx(ctx) { + if (m_ctx) cuCtxPushCurrent(m_ctx); + } + ~CudaCtx() { + if (m_ctx) cuCtxPopCurrent(&m_prev); + } + + private: + CUcontext m_ctx; + CUcontext m_prev = nullptr; +}; + } // namespace -VideoPlayer::VideoPlayer(EGLDisplay display) : m_eglDisplay(display) {} +NvidiaPlayer::NvidiaPlayer(EGLDisplay display) { + (void)display; +} -VideoPlayer::~VideoPlayer() { +NvidiaPlayer::~NvidiaPlayer() { stop(); std::lock_guard lock(m_mtx); + + CUcontext prev = nullptr; + if (m_cuCtx) cuCtxPushCurrent(m_cuCtx); + if (m_pending) { av_frame_unref(m_pending); av_frame_free(&m_pending); @@ -51,41 +73,39 @@ VideoPlayer::~VideoPlayer() { av_frame_free(&m_displayed); } + if (m_resY) { + cuGraphicsUnregisterResource(m_resY); + m_resY = nullptr; + } + if (m_resUV) { + cuGraphicsUnregisterResource(m_resUV); + m_resUV = nullptr; + } + if (m_texY) { + glDeleteTextures(1, &m_texY); + m_texY = 0; + } + if (m_texUV) { + glDeleteTextures(1, &m_texUV); + m_texUV = 0; + } + + if (m_cuCtx) cuCtxPopCurrent(&prev); + if (m_cuCtx) { - CUcontext prev = nullptr; - cuCtxPushCurrent(m_cuCtx); - if (m_resY) { - cuGraphicsUnregisterResource(m_resY); - m_resY = nullptr; - } - if (m_resUV) { - cuGraphicsUnregisterResource(m_resUV); - m_resUV = nullptr; - } - if (m_texY) { - glDeleteTextures(1, &m_texY); - m_texY = 0; - } - if (m_texUV) { - glDeleteTextures(1, &m_texUV); - m_texUV = 0; - } - cuCtxPopCurrent(&prev); cuCtxDestroy(m_cuCtx); m_cuCtx = nullptr; } m_texSize = QSize(); } -void VideoPlayer::setReadyCallback(std::function callback) { - m_readyCb = std::move(callback); +bool NvidiaPlayer::available() { + if (cuInit(0) != CUDA_SUCCESS) return false; + CUdevice dev = 0; + return cuDeviceGet(&dev, 0) == CUDA_SUCCESS; } -void VideoPlayer::setFrameCallback(std::function callback) { - m_frameCb = std::move(callback); -} - -bool VideoPlayer::openDecoder(const QString& path) { +bool NvidiaPlayer::openDecoder(const QString& path) { if (avformat_open_input( &m_fmt, path.toUtf8().constData(), nullptr, nullptr) < 0) { qCWarning( @@ -168,7 +188,7 @@ bool VideoPlayer::openDecoder(const QString& path) { return true; } -void VideoPlayer::closeDecoder() { +void NvidiaPlayer::closeDecoder() { if (m_codec) { avcodec_free_context(&m_codec); m_codec = nullptr; @@ -184,7 +204,7 @@ void VideoPlayer::closeDecoder() { m_streamIndex = -1; } -void VideoPlayer::decodeLoop(const QString& path, uint32_t generation) { +void NvidiaPlayer::decodeLoop(const QString& path, uint32_t generation) { if (!m_threadRunning.load(std::memory_order_acquire) || m_generation.load(std::memory_order_acquire) != generation) return; @@ -257,11 +277,16 @@ void VideoPlayer::decodeLoop(const QString& path, uint32_t generation) { } av_frame_free(&frame); - closeDecoder(); + { + CudaCtx ctxGuard(m_cuCtx); + closeDecoder(); + } m_threadRunning.store(false); } -bool VideoPlayer::start(const QString& path) { +bool NvidiaPlayer::start(const QString& path) { + if (!ensureCudaContext()) return false; + const uint32_t generation = m_generation.fetch_add(1, std::memory_order_acq_rel) + 1; m_threadRunning.store(true); @@ -271,26 +296,22 @@ bool VideoPlayer::start(const QString& path) { [this, path, generation, previous = std::move(previous)]() mutable { if (previous.joinable()) previous.join(); closeDecoder(); - - if (!ensureCudaContext()) { - m_threadRunning.store(false); - return; - } - decodeLoop(path, generation); }); return true; } -void VideoPlayer::stop() { +void NvidiaPlayer::stop() { m_generation.fetch_add(1, std::memory_order_acq_rel); m_threadRunning.store(false); if (m_thread.joinable()) m_thread.join(); + std::lock_guard lock(m_mtx); + CudaCtx ctxGuard(m_cuCtx); + closeDecoder(); - std::lock_guard lock(m_mtx); if (m_pending) { av_frame_unref(m_pending); m_pending = nullptr; @@ -303,12 +324,7 @@ void VideoPlayer::stop() { m_size = QSize(); } -QSize VideoPlayer::frameSize() const { - std::lock_guard lock(m_mtx); - return m_size; -} - -void VideoPlayer::deleteTexture() { +void NvidiaPlayer::deleteTexture() { std::lock_guard lock(m_mtx); CUcontext prev = nullptr; if (m_cuCtx) cuCtxPushCurrent(m_cuCtx); @@ -332,7 +348,7 @@ void VideoPlayer::deleteTexture() { m_texSize = QSize(); } -bool VideoPlayer::ensureCudaContext() { +bool NvidiaPlayer::ensureCudaContext() { if (m_cuCtx) return true; if (cuInit(0) != CUDA_SUCCESS) { @@ -354,7 +370,7 @@ bool VideoPlayer::ensureCudaContext() { return true; } -bool VideoPlayer::ensureTextures(unsigned w, unsigned h, int compBytes) { +bool NvidiaPlayer::ensureTextures(unsigned w, unsigned h, int compBytes) { if (!ensureCudaContext()) return false; CUcontext prev = nullptr; cuCtxPushCurrent(m_cuCtx); @@ -431,7 +447,7 @@ bool VideoPlayer::ensureTextures(unsigned w, unsigned h, int compBytes) { return true; } -bool VideoPlayer::importCurrentFrame() { +bool NvidiaPlayer::importCurrentFrame() { AVFrame* frame = nullptr; { std::lock_guard lock(m_mtx); @@ -546,4 +562,6 @@ bool VideoPlayer::importCurrentFrame() { return true; } +} // namespace ZShell::wallpaper + #endif diff --git a/Plugins/ZShell/Internal/videoplayer.hpp b/Plugins/ZShell/Wallpaper/nvidiaplayer.hpp similarity index 59% rename from Plugins/ZShell/Internal/videoplayer.hpp rename to Plugins/ZShell/Wallpaper/nvidiaplayer.hpp index ed7d6e4..823876e 100644 --- a/Plugins/ZShell/Internal/videoplayer.hpp +++ b/Plugins/ZShell/Wallpaper/nvidiaplayer.hpp @@ -1,11 +1,14 @@ #pragma once +#ifdef ZHAVE_VIDEO + +#include "videodecoder.hpp" + #include #include #include -#include #include #include @@ -23,27 +26,21 @@ extern "C" { #include } -class VideoPlayer { +namespace ZShell::wallpaper { + +class NvidiaPlayer : public VideoDecoder { public: - explicit VideoPlayer(EGLDisplay display); - ~VideoPlayer(); + explicit NvidiaPlayer(EGLDisplay display); + ~NvidiaPlayer() override; - void setReadyCallback(std::function callback); - void setFrameCallback(std::function callback); + [[nodiscard]] static bool available(); - bool start(const QString& path); - void stop(); + bool start(const QString& path) override; + void stop() override; + bool importCurrentFrame() override; + void deleteTexture() override; - - bool importCurrentFrame(); - - void deleteTexture(); - - [[nodiscard]] uint32_t textureId() const { return m_texY; } - [[nodiscard]] uint32_t chromaTextureId() const { return m_texUV; } - [[nodiscard]] int colorMatrix() const { return m_matrix; } - [[nodiscard]] int colorRange() const { return m_range; } - [[nodiscard]] QSize frameSize() const; + [[nodiscard]] const char* backendName() const override { return "nvidia"; } private: void decodeLoop(const QString& path, uint32_t generation); @@ -54,10 +51,6 @@ class VideoPlayer { bool ensureCudaContext(); - EGLDisplay m_eglDisplay; - std::function m_readyCb; - std::function m_frameCb; - std::thread m_thread; std::atomic m_threadRunning{false}; std::atomic m_generation{0}; @@ -77,12 +70,9 @@ class VideoPlayer { CUgraphicsResource m_resY = nullptr; CUgraphicsResource m_resUV = nullptr; QSize m_texSize; - - uint32_t m_texY = 0; - uint32_t m_texUV = 0; - - int m_matrix = 1; - int m_range = 0; int m_texComp = 1; - QSize m_size; }; + +} // namespace ZShell::wallpaper + +#endif // ZHAVE_VIDEO diff --git a/Plugins/ZShell/Internal/protocols/wlr-layer-shell-unstable-v1.xml b/Plugins/ZShell/Wallpaper/protocols/wlr-layer-shell-unstable-v1.xml similarity index 100% rename from Plugins/ZShell/Internal/protocols/wlr-layer-shell-unstable-v1.xml rename to Plugins/ZShell/Wallpaper/protocols/wlr-layer-shell-unstable-v1.xml diff --git a/Plugins/ZShell/Internal/shaders/wallpaper.frag b/Plugins/ZShell/Wallpaper/shaders/wallpaper.frag similarity index 100% rename from Plugins/ZShell/Internal/shaders/wallpaper.frag rename to Plugins/ZShell/Wallpaper/shaders/wallpaper.frag diff --git a/Plugins/ZShell/Internal/shaders/wallpaper.vert b/Plugins/ZShell/Wallpaper/shaders/wallpaper.vert similarity index 100% rename from Plugins/ZShell/Internal/shaders/wallpaper.vert rename to Plugins/ZShell/Wallpaper/shaders/wallpaper.vert diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_bowtie.frag b/Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_bowtie.frag similarity index 100% rename from Plugins/ZShell/Internal/shaders/wallpaper_trans_bowtie.frag rename to Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_bowtie.frag diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_doom_melt.frag b/Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_doom_melt.frag similarity index 100% rename from Plugins/ZShell/Internal/shaders/wallpaper_trans_doom_melt.frag rename to Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_doom_melt.frag diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_fade.frag b/Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_fade.frag similarity index 100% rename from Plugins/ZShell/Internal/shaders/wallpaper_trans_fade.frag rename to Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_fade.frag diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_hexagonalize.frag b/Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_hexagonalize.frag similarity index 100% rename from Plugins/ZShell/Internal/shaders/wallpaper_trans_hexagonalize.frag rename to Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_hexagonalize.frag diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_linear_blur.frag b/Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_linear_blur.frag similarity index 100% rename from Plugins/ZShell/Internal/shaders/wallpaper_trans_linear_blur.frag rename to Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_linear_blur.frag diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_split_slide_in_horizontal.frag b/Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_split_slide_in_horizontal.frag similarity index 100% rename from Plugins/ZShell/Internal/shaders/wallpaper_trans_split_slide_in_horizontal.frag rename to Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_split_slide_in_horizontal.frag diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_tiles_wave.frag b/Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_tiles_wave.frag similarity index 100% rename from Plugins/ZShell/Internal/shaders/wallpaper_trans_tiles_wave.frag rename to Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_tiles_wave.frag diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_window_slices.frag b/Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_window_slices.frag similarity index 100% rename from Plugins/ZShell/Internal/shaders/wallpaper_trans_window_slices.frag rename to Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_window_slices.frag diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_wipe_down.frag b/Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_wipe_down.frag similarity index 100% rename from Plugins/ZShell/Internal/shaders/wallpaper_trans_wipe_down.frag rename to Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_wipe_down.frag diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_trans_wipe_right.frag b/Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_wipe_right.frag similarity index 100% rename from Plugins/ZShell/Internal/shaders/wallpaper_trans_wipe_right.frag rename to Plugins/ZShell/Wallpaper/shaders/wallpaper_trans_wipe_right.frag diff --git a/Plugins/ZShell/Internal/shaders/wallpaper_video.frag b/Plugins/ZShell/Wallpaper/shaders/wallpaper_video.frag similarity index 100% rename from Plugins/ZShell/Internal/shaders/wallpaper_video.frag rename to Plugins/ZShell/Wallpaper/shaders/wallpaper_video.frag diff --git a/Plugins/ZShell/Wallpaper/vaapiplayer.cpp b/Plugins/ZShell/Wallpaper/vaapiplayer.cpp new file mode 100644 index 0000000..bd6ea2c --- /dev/null +++ b/Plugins/ZShell/Wallpaper/vaapiplayer.cpp @@ -0,0 +1,628 @@ +#define EGL_EGLEXT_PROTOTYPES +#include "vaapiplayer.hpp" + +#ifdef ZHAVE_FFMPEG + +#define GL_GLEXT_PROTOTYPES + +#include +#include +#include + +#ifdef ZHAVE_VAAPI +#include +extern "C" { +#include +#include +#include +} +#ifndef DRM_FORMAT_MOD_INVALID +#define DRM_FORMAT_MOD_INVALID 0xffffffffffffffffULL +#endif +#endif + +#include +#include +#include + +#include + +Q_LOGGING_CATEGORY(vaapiLog, "ZShell.Internal.wallpaper") + +namespace ZShell::wallpaper { + +namespace { + +enum AVPixelFormat chooseFormat( + AVCodecContext* ctx, const AVPixelFormat* pix_fmts) { + (void)ctx; + for (const AVPixelFormat* p = pix_fmts; *p != AV_PIX_FMT_NONE; ++p) + if (*p == AV_PIX_FMT_VAAPI) return AV_PIX_FMT_VAAPI; + return pix_fmts[0]; +} + +#ifdef ZHAVE_VAAPI +using GLEGLImageTargetProc = void (*)(GLenum, EGLImageKHR); + +GLEGLImageTargetProc glEGLImageTarget() { + static GLEGLImageTargetProc fn = reinterpret_cast( + eglGetProcAddress("glEGLImageTargetTexture2DOES")); + return fn; +} + +EGLImageKHR createDmaImage( + EGLDisplay dpy, + int fd, + uint32_t offset, + uint32_t pitch, + uint32_t fourcc, + uint64_t modifier) { + EGLint attrs[16]; + int n = 0; + attrs[n++] = EGL_LINUX_DRM_FOURCC_EXT; + attrs[n++] = EGLint(fourcc); + attrs[n++] = EGL_DMA_BUF_PLANE0_FD_EXT; + attrs[n++] = fd; + attrs[n++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT; + attrs[n++] = EGLint(offset); + attrs[n++] = EGL_DMA_BUF_PLANE0_PITCH_EXT; + attrs[n++] = EGLint(pitch); + if (modifier != DRM_FORMAT_MOD_INVALID) { + attrs[n++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT; + attrs[n++] = EGLint(modifier & 0xffffffffULL); + attrs[n++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT; + attrs[n++] = EGLint(modifier >> 32); + } + attrs[n] = EGL_NONE; + + EGLImageKHR image = eglCreateImageKHR( + dpy, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attrs); + if (image == EGL_NO_IMAGE_KHR) return EGL_NO_IMAGE_KHR; + return image; +} +#endif + +} // namespace + +VaapiPlayer::VaapiPlayer(EGLDisplay display) : m_eglDisplay(display) {} + +VaapiPlayer::~VaapiPlayer() { + stop(); + + std::lock_guard lock(m_mtx); + if (m_pending) { + av_frame_unref(m_pending); + av_frame_free(&m_pending); + } + if (m_displayed) { + av_frame_unref(m_displayed); + av_frame_free(&m_displayed); + } + if (m_hwDeviceCtx) { + av_buffer_unref(&m_hwDeviceCtx); + m_hwDeviceCtx = nullptr; + } + +#ifdef ZHAVE_VAAPI + if (m_imageY != EGL_NO_IMAGE_KHR) { + eglDestroyImageKHR(m_eglDisplay, m_imageY); + m_imageY = EGL_NO_IMAGE_KHR; + } + if (m_imageUV != EGL_NO_IMAGE_KHR) { + eglDestroyImageKHR(m_eglDisplay, m_imageUV); + m_imageUV = EGL_NO_IMAGE_KHR; + } +#endif +} + +bool VaapiPlayer::available() { + AVBufferRef* ref = nullptr; + if (av_hwdevice_ctx_create( + &ref, AV_HWDEVICE_TYPE_VAAPI, nullptr, nullptr, 0) < 0) { + if (ref) av_buffer_unref(&ref); + return false; + } + av_buffer_unref(&ref); + return true; +} + +bool VaapiPlayer::ensureDevice() { + if (m_hwDeviceCtx) return true; + if (av_hwdevice_ctx_create( + &m_hwDeviceCtx, AV_HWDEVICE_TYPE_VAAPI, nullptr, nullptr, 0) < 0) { + if (m_hwDeviceCtx) av_buffer_unref(&m_hwDeviceCtx); + m_hwDeviceCtx = nullptr; + qCWarning(vaapiLog, "VideoPlayer: could not create vaapi hwdevice"); + return false; + } + return true; +} + +bool VaapiPlayer::openDecoder(const QString& path) { + if (avformat_open_input( + &m_fmt, path.toUtf8().constData(), nullptr, nullptr) < 0) { + qCWarning( + vaapiLog, + "VideoPlayer: could not open %s", + path.toUtf8().constData()); + return false; + } + if (avformat_find_stream_info(m_fmt, nullptr) < 0) { + qCWarning(vaapiLog, "VideoPlayer: could not read stream info"); + return false; + } + + m_streamIndex = -1; + for (unsigned i = 0; i < m_fmt->nb_streams; ++i) + if (m_fmt->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + m_streamIndex = int(i); + break; + } + if (m_streamIndex < 0) { + qCWarning(vaapiLog, "VideoPlayer: no video stream"); + return false; + } + + AVCodecParameters* par = m_fmt->streams[m_streamIndex]->codecpar; + const AVCodec* codec = avcodec_find_decoder(par->codec_id); + const AVCodec* hwcodec = nullptr; + void* it = nullptr; + while (const AVCodec* c = av_codec_iterate(&it)) { + if (c->id != par->codec_id || !av_codec_is_decoder(c)) continue; + for (int i = 0; + const AVCodecHWConfig* cfg = avcodec_get_hw_config(c, i); + ++i) + if (cfg->device_type == AV_HWDEVICE_TYPE_VAAPI && + (cfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) { + hwcodec = c; + break; + } + if (hwcodec) break; + } + if (hwcodec) codec = hwcodec; + if (!codec) { + qCWarning( + vaapiLog, + "VideoPlayer: no decoder for codec id %d", + int(par->codec_id)); + return false; + } + + m_codec = avcodec_alloc_context3(codec); + if (!m_codec) return false; + if (avcodec_parameters_to_context(m_codec, par) < 0) { + qCWarning(vaapiLog, "VideoPlayer: could not copy codec params"); + return false; + } + + if (!ensureDevice()) { + return false; + } + m_codec->hw_device_ctx = av_buffer_ref(m_hwDeviceCtx); + m_codec->get_format = chooseFormat; + + if (avcodec_open2(m_codec, codec, nullptr) < 0) { + qCWarning(vaapiLog, "VideoPlayer: could not open codec"); + return false; + } + + return true; +} + +void VaapiPlayer::closeDecoder() { + if (m_codec) { + avcodec_free_context(&m_codec); + m_codec = nullptr; + } + if (m_fmt) { + avformat_close_input(&m_fmt); + m_fmt = nullptr; + } + m_streamIndex = -1; +} + +void VaapiPlayer::decodeLoop(const QString& path, uint32_t generation) { + if (!m_threadRunning.load(std::memory_order_acquire) || + m_generation.load(std::memory_order_acquire) != generation) + return; + + if (!openDecoder(path)) { + m_threadRunning.store(false); + return; + } + + AVRational fr = m_fmt->streams[m_streamIndex]->avg_frame_rate; + if (fr.num <= 0 || fr.den <= 0) fr = m_codec->framerate; + if (fr.num <= 0 || fr.den <= 0) fr = AVRational{30, 1}; + const int64_t intervalUs = + av_rescale_q(1, AVRational{fr.den, fr.num}, AVRational{1, 1000000}); + + AVFrame* frame = av_frame_alloc(); + if (!frame) { + m_threadRunning.store(false); + return; + } + + AVPacket pkt{}; + + while (m_threadRunning.load(std::memory_order_acquire) && + m_generation.load(std::memory_order_acquire) == generation) { + int ret = av_read_frame(m_fmt, &pkt); + if (ret == AVERROR_EOF) { + if (avformat_seek_file( + m_fmt, m_streamIndex, INT64_MIN, 0, INT64_MAX, 0) < 0) { + qCWarning(vaapiLog, "VideoPlayer: loop seek failed; stopping"); + break; + } + avcodec_flush_buffers(m_codec); + continue; + } + if (ret < 0) { + qCWarning(vaapiLog, "VideoPlayer: read error; stopping"); + break; + } + if (pkt.stream_index != m_streamIndex) { + av_packet_unref(&pkt); + continue; + } + + if (avcodec_send_packet(m_codec, &pkt) < 0) { + av_packet_unref(&pkt); + continue; + } + av_packet_unref(&pkt); + + while (m_threadRunning.load(std::memory_order_acquire) && + m_generation.load(std::memory_order_acquire) == generation && + avcodec_receive_frame(m_codec, frame) == 0) { + bool wasEmpty = false; + { + std::lock_guard lock(m_mtx); + if (!m_pending) m_pending = av_frame_alloc(); + av_frame_unref(m_pending); + if (av_frame_ref(m_pending, frame) >= 0) { + wasEmpty = !m_haveFrame; + m_haveFrame = true; + } + } + if (wasEmpty && m_readyCb) m_readyCb(); + if (m_frameCb) m_frameCb(); + av_frame_unref(frame); + } + + std::this_thread::sleep_for(std::chrono::microseconds(intervalUs)); + } + + av_frame_free(&frame); + closeDecoder(); + m_threadRunning.store(false); +} + +bool VaapiPlayer::start(const QString& path) { + if (!ensureDevice()) return false; + + const uint32_t generation = + m_generation.fetch_add(1, std::memory_order_acq_rel) + 1; + m_threadRunning.store(true); + + std::thread previous = std::move(m_thread); + m_thread = std::thread( + [this, path, generation, previous = std::move(previous)]() mutable { + if (previous.joinable()) previous.join(); + closeDecoder(); + decodeLoop(path, generation); + }); + + return true; +} + +void VaapiPlayer::stop() { + m_generation.fetch_add(1, std::memory_order_acq_rel); + m_threadRunning.store(false); + if (m_thread.joinable()) m_thread.join(); + + closeDecoder(); + + std::lock_guard lock(m_mtx); + if (m_pending) { + av_frame_unref(m_pending); + m_pending = nullptr; + } + if (m_displayed) { + av_frame_unref(m_displayed); + m_displayed = nullptr; + } + m_haveFrame = false; + m_size = QSize(); +} + +void VaapiPlayer::deleteTexture() { +#ifdef ZHAVE_VAAPI + if (m_imageY != EGL_NO_IMAGE_KHR) { + eglDestroyImageKHR(m_eglDisplay, m_imageY); + m_imageY = EGL_NO_IMAGE_KHR; + } + if (m_imageUV != EGL_NO_IMAGE_KHR) { + eglDestroyImageKHR(m_eglDisplay, m_imageUV); + m_imageUV = EGL_NO_IMAGE_KHR; + } +#endif + if (m_texY) { + glDeleteTextures(1, &m_texY); + m_texY = 0; + } + if (m_texUV) { + glDeleteTextures(1, &m_texUV); + m_texUV = 0; + } + m_texSize = QSize(); +} + +bool VaapiPlayer::ensureTextures(unsigned w, unsigned h, int compBytes) { + const QSize sz = QSize(int(w), int(h)); + if (m_texSize == sz && m_texComp == compBytes && m_texY != 0) return true; + + if (m_texY) glDeleteTextures(1, &m_texY); + if (m_texUV) glDeleteTextures(1, &m_texUV); + m_texY = m_texUV = 0; + + const bool wide = compBytes == 2; + auto makeTex = + [&](GLuint& tex, GLint internal, GLenum fmt, unsigned tw, unsigned th) { + glGenTextures(1, &tex); + glBindTexture(GL_TEXTURE_2D, tex); + glTexImage2D( + GL_TEXTURE_2D, + 0, + internal, + GLint(tw), + GLint(th), + 0, + fmt, + wide ? GL_UNSIGNED_SHORT : GL_UNSIGNED_BYTE, + nullptr); + 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); + }; + makeTex(m_texY, wide ? GL_R16 : GL_R8, GL_RED, w, h); + makeTex(m_texUV, wide ? GL_RG16 : GL_RG8, GL_RG, w / 2, h / 2); + + m_texSize = sz; + m_texComp = compBytes; + return true; +} + +bool VaapiPlayer::importCurrentFrame() { + AVFrame* frame = nullptr; + { + std::lock_guard lock(m_mtx); + if (!m_pending || m_pending->format == AV_PIX_FMT_NONE) return false; + frame = m_pending; + m_pending = nullptr; + } + + if (frame->format != AV_PIX_FMT_VAAPI || !frame->hw_frames_ctx) { + static int logged = 0; + if (logged++ < 3) + qCWarning( + vaapiLog, + "VideoPlayer: frame not on GPU (fmt=%s hw_frames=%d)", + av_get_pix_fmt_name(AVPixelFormat(frame->format)), + frame->hw_frames_ctx != nullptr); + av_frame_unref(frame); + return false; + } + + const AVHWFramesContext* hf = + reinterpret_cast(frame->hw_frames_ctx->data); + const AVPixelFormat sw = hf->sw_format; + const bool tenBit = sw == AV_PIX_FMT_P010LE || sw == AV_PIX_FMT_P010BE || + sw == AV_PIX_FMT_P012LE || sw == AV_PIX_FMT_P016LE; + const int compBytes = tenBit ? 2 : 1; + const size_t compSize = size_t(compBytes); + + const unsigned w = unsigned(frame->width); + const unsigned h = unsigned(frame->height); + if (w == 0 || h == 0) { + av_frame_unref(frame); + return false; + } + + switch (frame->colorspace) { + case AVCOL_SPC_BT470BG: + case AVCOL_SPC_SMPTE170M: + m_matrix = 0; + break; + case AVCOL_SPC_BT709: + m_matrix = 1; + break; + default: + m_matrix = h >= 720 ? 1 : 0; + break; + } + m_range = frame->color_range == AVCOL_RANGE_JPEG ? 1 : 0; + m_size = QSize(int(w), int(h)); + +#ifdef ZHAVE_VAAPI + if (importEglImages(frame)) { + std::lock_guard lock(m_mtx); + if (m_displayed) av_frame_unref(m_displayed); + m_displayed = frame; + return true; + } +#endif + + AVFrame* tmp = av_frame_alloc(); + if (!tmp) { + av_frame_unref(frame); + return false; + } + if (av_hwframe_transfer_data(tmp, frame, 0) < 0) { + av_frame_free(&tmp); + av_frame_unref(frame); + return false; + } + + size_t pitchY = size_t(tmp->linesize[0]); + size_t pitchUV = tmp->linesize[1] > 0 ? size_t(tmp->linesize[1]) : pitchY; + if (pitchY == 0) pitchY = size_t(w) * compSize; + + if (!ensureTextures(w, h, compBytes)) { + av_frame_free(&tmp); + av_frame_unref(frame); + return false; + } + + const GLenum type = compBytes == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_BYTE; + + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + + glBindTexture(GL_TEXTURE_2D, m_texY); + glPixelStorei(GL_UNPACK_ROW_LENGTH, GLint(pitchY / compSize)); + glTexSubImage2D( + GL_TEXTURE_2D, 0, 0, 0, GLint(w), GLint(h), GL_RED, type, tmp->data[0]); + + glBindTexture(GL_TEXTURE_2D, m_texUV); + glPixelStorei(GL_UNPACK_ROW_LENGTH, GLint(pitchUV / (compSize * 2))); + glTexSubImage2D( + GL_TEXTURE_2D, + 0, + 0, + 0, + GLint(w / 2), + GLint(h / 2), + GL_RG, + type, + tmp->data[1]); + + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + + av_frame_free(&tmp); + + { + std::lock_guard lock(m_mtx); + if (m_displayed) av_frame_unref(m_displayed); + m_displayed = frame; + } + + return true; +} + +#ifdef ZHAVE_VAAPI +bool VaapiPlayer::importEglImages(AVFrame* frame) { + if (m_eglDisplay == EGL_NO_DISPLAY) return false; + GLEGLImageTargetProc target = glEGLImageTarget(); + if (!target) return false; + + const AVHWFramesContext* hf = + reinterpret_cast(frame->hw_frames_ctx->data); + const AVHWDeviceContext* dev = hf->device_ctx; + const AVVAAPIDeviceContext* va = + reinterpret_cast(dev->hwctx); + if (!va->display) return false; + + const VASurfaceID surface = VASurfaceID(uintptr_t(frame->data[0])); + + VADRMPRIMESurfaceDescriptor desc{}; + VAStatus st = vaExportSurfaceHandle( + va->display, + surface, + VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, + VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS, + &desc); + if (st != VA_STATUS_SUCCESS) { + static int logged = 0; + if (logged++ < 3) + qCWarning( + vaapiLog, + "VideoPlayer: vaExportSurfaceHandle failed (%d); CPU fallback", + int(st)); + return false; + } + + if (desc.num_layers != 2 || desc.num_objects < 2) { + for (uint32_t i = 0; i < desc.num_objects; ++i) + if (desc.objects[i].fd >= 0) close(desc.objects[i].fd); + return false; + } + + const auto bindLayer = [&](uint32_t layer, GLuint tex) -> EGLImageKHR { + const auto& L = desc.layers[layer]; + if (L.num_planes < 1) return EGL_NO_IMAGE_KHR; + const auto& obj = desc.objects[L.object_index[0]]; + if (obj.fd < 0) return EGL_NO_IMAGE_KHR; + EGLImageKHR image = createDmaImage( + m_eglDisplay, + obj.fd, + L.offset[0], + L.pitch[0], + L.drm_format, + obj.drm_format_modifier); + if (image == EGL_NO_IMAGE_KHR) return EGL_NO_IMAGE_KHR; + glBindTexture(GL_TEXTURE_2D, tex); + target(GL_TEXTURE_2D, image); + return image; + }; + + const QSize sz = QSize(int(frame->width), int(frame->height)); + if (m_texSize != sz || m_texY == 0) { + if (m_imageY != EGL_NO_IMAGE_KHR) { + eglDestroyImageKHR(m_eglDisplay, m_imageY); + m_imageY = EGL_NO_IMAGE_KHR; + } + if (m_imageUV != EGL_NO_IMAGE_KHR) { + eglDestroyImageKHR(m_eglDisplay, m_imageUV); + m_imageUV = EGL_NO_IMAGE_KHR; + } + if (m_texY == 0) glGenTextures(1, &m_texY); + if (m_texUV == 0) glGenTextures(1, &m_texUV); + glBindTexture(GL_TEXTURE_2D, m_texY); + 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); + glBindTexture(GL_TEXTURE_2D, m_texUV); + 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); + m_texSize = sz; + } + + EGLImageKHR imageY = bindLayer(0, m_texY); + EGLImageKHR imageUV = bindLayer(1, m_texUV); + + if (imageY == EGL_NO_IMAGE_KHR || imageUV == EGL_NO_IMAGE_KHR) { + if (imageY != EGL_NO_IMAGE_KHR) + eglDestroyImageKHR(m_eglDisplay, imageY); + if (imageUV != EGL_NO_IMAGE_KHR) + eglDestroyImageKHR(m_eglDisplay, imageUV); + for (uint32_t i = 0; i < desc.num_objects; ++i) + if (desc.objects[i].fd >= 0) close(desc.objects[i].fd); + static int logged = 0; + if (logged++ < 3) + qCWarning( + vaapiLog, "VideoPlayer: EGLImage import failed; CPU fallback"); + return false; + } + + if (m_imageY != EGL_NO_IMAGE_KHR) + eglDestroyImageKHR(m_eglDisplay, m_imageY); + if (m_imageUV != EGL_NO_IMAGE_KHR) + eglDestroyImageKHR(m_eglDisplay, m_imageUV); + + m_imageY = imageY; + m_imageUV = imageUV; + + for (uint32_t i = 0; i < desc.num_objects; ++i) + if (desc.objects[i].fd >= 0) close(desc.objects[i].fd); + + return true; +} +#endif + +} // namespace ZShell::wallpaper + +#endif diff --git a/Plugins/ZShell/Wallpaper/vaapiplayer.hpp b/Plugins/ZShell/Wallpaper/vaapiplayer.hpp new file mode 100644 index 0000000..aacf26a --- /dev/null +++ b/Plugins/ZShell/Wallpaper/vaapiplayer.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include "videodecoder.hpp" + +#include + +#include +#include +#include + +#include +#include + +#include +#ifdef ZHAVE_VAAPI +#include +#endif + +extern "C" { +#include +#include +#include +#include +#include +#include +#include +} + +namespace ZShell::wallpaper { + +class VaapiPlayer : public VideoDecoder { + public: + explicit VaapiPlayer(EGLDisplay display); + ~VaapiPlayer() override; + + [[nodiscard]] static bool available(); + + bool start(const QString& path) override; + void stop() override; + bool importCurrentFrame() override; + void deleteTexture() override; + + [[nodiscard]] const char* backendName() const override { return "vaapi"; } + + private: + void decodeLoop(const QString& path, uint32_t generation); + bool openDecoder(const QString& path); + void closeDecoder(); + + bool ensureDevice(); + bool ensureTextures(unsigned w, unsigned h, int compBytes); + +#ifdef ZHAVE_VAAPI + bool importEglImages(AVFrame* frame); +#endif + + EGLDisplay m_eglDisplay = EGL_NO_DISPLAY; + + std::thread m_thread; + std::atomic m_threadRunning{false}; + std::atomic m_generation{0}; + + mutable std::mutex m_mtx; + AVFrame* m_pending = nullptr; + bool m_haveFrame = false; + + AVFrame* m_displayed = nullptr; + + AVFormatContext* m_fmt = nullptr; + AVCodecContext* m_codec = nullptr; + AVBufferRef* m_hwDeviceCtx = nullptr; + int m_streamIndex = -1; + + QSize m_texSize; + int m_texComp = 1; + +#ifdef ZHAVE_VAAPI + EGLImageKHR m_imageY = EGL_NO_IMAGE_KHR; + EGLImageKHR m_imageUV = EGL_NO_IMAGE_KHR; +#endif +}; + +} // namespace ZShell::wallpaper diff --git a/Plugins/ZShell/Wallpaper/videodecoder.cpp b/Plugins/ZShell/Wallpaper/videodecoder.cpp new file mode 100644 index 0000000..52e5e67 --- /dev/null +++ b/Plugins/ZShell/Wallpaper/videodecoder.cpp @@ -0,0 +1,117 @@ +#include "videodecoder.hpp" + +#include + +#ifdef ZHAVE_VIDEO +#include "nvidiaplayer.hpp" +#endif + +#ifdef ZHAVE_FFMPEG +#include "vaapiplayer.hpp" +#endif + +namespace ZShell::wallpaper { + +VideoDecoder::~VideoDecoder() = default; + +void VideoDecoder::setReadyCallback(std::function callback) { + m_readyCb = std::move(callback); +} + +void VideoDecoder::setFrameCallback(std::function callback) { + m_frameCb = std::move(callback); +} + +uint32_t VideoDecoder::textureId() const { + return m_texY; +} + +uint32_t VideoDecoder::chromaTextureId() const { + return m_texUV; +} + +int VideoDecoder::colorMatrix() const { + return m_matrix; +} + +int VideoDecoder::colorRange() const { + return m_range; +} + +QSize VideoDecoder::frameSize() const { + return m_size; +} + +namespace { + +VideoBackend backendFromEnvironment() { + if (qEnvironmentVariableIsSet("ZBAR_WALLPAPER_VAAPI")) + return VideoBackend::Vaapi; + if (qEnvironmentVariableIsSet("ZBAR_WALLPAPER_NVIDIA")) + return VideoBackend::Nvidia; + return VideoBackend::Auto; +} + +VideoBackend selectBackend(VideoBackend hint) { + if (hint == VideoBackend::None) return VideoBackend::None; + if (hint == VideoBackend::Auto) hint = backendFromEnvironment(); + + if (hint == VideoBackend::Nvidia) { +#ifdef ZHAVE_VIDEO + if (NvidiaPlayer::available()) return VideoBackend::Nvidia; +#endif + return VideoBackend::None; + } + + if (hint == VideoBackend::Vaapi) { +#ifdef ZHAVE_FFMPEG + if (VaapiPlayer::available()) return VideoBackend::Vaapi; +#endif + return VideoBackend::None; + } + +#ifdef ZHAVE_VIDEO + if (NvidiaPlayer::available()) return VideoBackend::Nvidia; +#endif + +#ifdef ZHAVE_FFMPEG + if (VaapiPlayer::available()) return VideoBackend::Vaapi; +#endif + + return VideoBackend::None; +} + +} // namespace + +std::unique_ptr createVideoDecoder( + EGLDisplay display, VideoBackend hint) { + switch (selectBackend(hint)) { +#ifdef ZHAVE_VIDEO + case VideoBackend::Nvidia: + return std::make_unique(display); +#endif +#ifdef ZHAVE_FFMPEG + case VideoBackend::Vaapi: + return std::make_unique(display); +#endif + case VideoBackend::Auto: + case VideoBackend::None: + break; + } + return nullptr; +} + +const char* resolveBackendName(VideoBackend hint) { + switch (selectBackend(hint)) { + case VideoBackend::Nvidia: + return "nvidia"; + case VideoBackend::Vaapi: + return "vaapi"; + case VideoBackend::Auto: + case VideoBackend::None: + break; + } + return "mpv"; +} + +} // namespace ZShell::wallpaper diff --git a/Plugins/ZShell/Wallpaper/videodecoder.hpp b/Plugins/ZShell/Wallpaper/videodecoder.hpp new file mode 100644 index 0000000..2275aaf --- /dev/null +++ b/Plugins/ZShell/Wallpaper/videodecoder.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include +#include +#include + +#include +#include + +namespace ZShell::wallpaper { + +class VideoDecoder { + public: + virtual ~VideoDecoder(); + + void setReadyCallback(std::function callback); + void setFrameCallback(std::function callback); + + virtual bool start(const QString& path) = 0; + virtual void stop() = 0; + + virtual bool importCurrentFrame() = 0; + virtual void deleteTexture() = 0; + + [[nodiscard]] uint32_t textureId() const; + [[nodiscard]] uint32_t chromaTextureId() const; + [[nodiscard]] int colorMatrix() const; + [[nodiscard]] int colorRange() const; + [[nodiscard]] QSize frameSize() const; + + [[nodiscard]] virtual const char* backendName() const = 0; + + protected: + std::function m_readyCb; + std::function m_frameCb; + + uint32_t m_texY = 0; + uint32_t m_texUV = 0; + int m_matrix = 1; + int m_range = 0; + QSize m_size; +}; + +enum class VideoBackend { + Auto, + Nvidia, + Vaapi, + None, +}; + +std::unique_ptr createVideoDecoder( + EGLDisplay display, VideoBackend hint = VideoBackend::Auto); + +const char* resolveBackendName(VideoBackend hint = VideoBackend::Auto); + +} // namespace ZShell::wallpaper diff --git a/Plugins/ZShell/Wallpaper/videotranscoder.cpp b/Plugins/ZShell/Wallpaper/videotranscoder.cpp new file mode 100644 index 0000000..5e998ab --- /dev/null +++ b/Plugins/ZShell/Wallpaper/videotranscoder.cpp @@ -0,0 +1,165 @@ +#include "videotranscoder.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +Q_LOGGING_CATEGORY(transcodeLog, "ZShell.Internal.wallpaper") + +namespace ZShell::wallpaper { + +VideoTranscoder::VideoTranscoder(QObject* parent) : QObject(parent) {} + +bool VideoTranscoder::isHardwareCodec(const QString& codec) { + static const QStringList hardware{ + "av1", + "hevc", + "h264", + "vp9", + "vp8", + "vc1", + "mpeg2", + "mpeg2video", + "mpeg4", + "mjpeg", + }; + + return hardware.contains(codec); +} + +QString VideoTranscoder::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(transcodeLog) << "WallpaperService: ffprobe failed for" << path + << "(exit" << probe.exitCode() << ")"; + return {}; + } + + return QString::fromUtf8(probe.readAllStandardOutput()).trimmed().toLower(); +} + +QString VideoTranscoder::cacheDir() const { + return QDir::homePath() + "/.cache/zshell/wallpaper-videos"; +} + +QString VideoTranscoder::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 VideoTranscoder::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 VideoTranscoder::start(const QString& src, const QString& dst) { + QDir().mkpath(cacheDir()); + + if (m_process) { + m_process->kill(); + m_process->deleteLater(); + m_process = nullptr; + } + + m_process = new QProcess(this); + auto* proc = m_process; + + connect(proc, &QProcess::readyReadStandardError, proc, [proc]() { + qWarning(transcodeLog).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(transcodeLog) + << "WallpaperService: transcode finished, exit" << code + << "status" << int(status) << "exists" << QFile::exists(dst) + << "->" << dst; + + if (proc == m_process) m_process = nullptr; + proc->deleteLater(); + + emit transcoded(dst, ok); + }); + + qWarning(transcodeLog) << "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(); +} + +} // namespace ZShell::wallpaper diff --git a/Plugins/ZShell/Wallpaper/videotranscoder.hpp b/Plugins/ZShell/Wallpaper/videotranscoder.hpp new file mode 100644 index 0000000..9879d6f --- /dev/null +++ b/Plugins/ZShell/Wallpaper/videotranscoder.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +class QProcess; + +namespace ZShell::wallpaper { + +class VideoTranscoder : public QObject { + Q_OBJECT + + public: + explicit VideoTranscoder(QObject* parent = nullptr); + + [[nodiscard]] QString cacheDir() const; + [[nodiscard]] QString transcodeTarget(const QString& path) const; + [[nodiscard]] bool needsTranscode(const QString& path) const; + + void start(const QString& src, const QString& dst); + + static bool isHardwareCodec(const QString& codec); + + signals: + void transcoded(const QString& dst, bool ok); + + private: + [[nodiscard]] QString codecName(const QString& path) const; + + QProcess* m_process = nullptr; +}; + +} // namespace ZShell::wallpaper diff --git a/Plugins/ZShell/Wallpaper/wallpapercache.cpp b/Plugins/ZShell/Wallpaper/wallpapercache.cpp new file mode 100644 index 0000000..a0aeb2e --- /dev/null +++ b/Plugins/ZShell/Wallpaper/wallpapercache.cpp @@ -0,0 +1,170 @@ +#include "wallpapercache.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ZShell::wallpaper { + +namespace { + +QString connectionName() { + return QStringLiteral("wallpaper-cache-%1") + .arg(quintptr(QThread::currentThreadId())); +} + +QSqlDatabase openDatabase(const QString& databasePath) { + const QString connection = connectionName(); + + 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 {}; + return database; +} + +} // namespace + +void WallpaperCache::setDbPath(const QString& path) { + std::lock_guard lock(m_mtx); + m_dbPath = path; +} + +QString WallpaperCache::dbPath() const { + std::lock_guard lock(m_mtx); + return m_dbPath; +} + +QByteArray WallpaperCache::cachedBlob(const QString& path) const { + if (path.isEmpty()) return {}; + + const QString databasePath = dbPath(); + if (databasePath.isEmpty()) return {}; + + const QFileInfo info(path); + + QSqlDatabase database = openDatabase(databasePath); + if (!database.isOpen()) 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 WallpaperCache::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()) return {}; + + return image.convertToFormat(QImage::Format_RGBA8888); +} + +QByteArray WallpaperCache::posterBlob(const QString& path) const { + if (path.isEmpty()) return {}; + + const QString databasePath = dbPath(); + if (databasePath.isEmpty()) return {}; + + const QFileInfo info(path); + + QSqlDatabase database = openDatabase(databasePath); + if (!database.isOpen()) return {}; + + QSqlQuery query(database); + query.prepare("SELECT poster, 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; +} + +QVariantMap WallpaperCache::cropsForPath(const QString& path) const { + if (path.isEmpty()) return {}; + + const QString databasePath = dbPath(); + if (databasePath.isEmpty()) return {}; + + const QFileInfo info(path); + + QSqlDatabase database = openDatabase(databasePath); + if (!database.isOpen()) return {}; + + QSqlQuery query(database); + query.prepare("SELECT crop FROM wallpapers WHERE path = ?"); + query.addBindValue(info.absoluteFilePath()); + if (!query.exec() || !query.next()) return {}; + + const QByteArray json = query.value(0).toByteArray(); + if (json.isEmpty()) return {}; + + const auto doc = QJsonDocument::fromJson(json); + if (!doc.isObject()) return {}; + + return doc.object().toVariantMap(); +} + +} // namespace ZShell::wallpaper diff --git a/Plugins/ZShell/Wallpaper/wallpapercache.hpp b/Plugins/ZShell/Wallpaper/wallpapercache.hpp new file mode 100644 index 0000000..a0f6ad3 --- /dev/null +++ b/Plugins/ZShell/Wallpaper/wallpapercache.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace ZShell::wallpaper { + +class WallpaperCache { + public: + void setDbPath(const QString& path); + [[nodiscard]] QString dbPath() const; + + [[nodiscard]] QImage decodeImage( + const QString& path, const QSize& decodeSize) const; + + [[nodiscard]] QByteArray posterBlob(const QString& path) const; + + [[nodiscard]] QVariantMap cropsForPath(const QString& path) const; + + private: + [[nodiscard]] QByteArray cachedBlob(const QString& path) const; + + mutable std::mutex m_mtx; + QString m_dbPath; +}; + +} // namespace ZShell::wallpaper diff --git a/Plugins/ZShell/Internal/wallpaperimage.cpp b/Plugins/ZShell/Wallpaper/wallpaperimage.cpp similarity index 99% rename from Plugins/ZShell/Internal/wallpaperimage.cpp rename to Plugins/ZShell/Wallpaper/wallpaperimage.cpp index f6de9de..897dcb2 100644 --- a/Plugins/ZShell/Internal/wallpaperimage.cpp +++ b/Plugins/ZShell/Wallpaper/wallpaperimage.cpp @@ -8,7 +8,7 @@ #include #include -namespace ZShell::internal { +namespace ZShell::wallpaper { QThreadPool* WallpaperImage::loadPool() { static QThreadPool* pool = [] { @@ -352,4 +352,4 @@ QSGNode* WallpaperImage::updatePaintNode( return m_rootNode; } -} // namespace ZShell::internal +} // namespace ZShell::wallpaper diff --git a/Plugins/ZShell/Internal/wallpaperimage.hpp b/Plugins/ZShell/Wallpaper/wallpaperimage.hpp similarity index 98% rename from Plugins/ZShell/Internal/wallpaperimage.hpp rename to Plugins/ZShell/Wallpaper/wallpaperimage.hpp index 6ecf562..8226759 100644 --- a/Plugins/ZShell/Internal/wallpaperimage.hpp +++ b/Plugins/ZShell/Wallpaper/wallpaperimage.hpp @@ -14,7 +14,7 @@ #include #include -namespace ZShell::internal { +namespace ZShell::wallpaper { class WallpaperImage : public QQuickItem { Q_OBJECT @@ -144,4 +144,4 @@ class WallpaperImage : public QQuickItem { QSGImageNode* m_oldImageNode = nullptr; }; -} // namespace ZShell::internal +} // namespace ZShell::wallpaper diff --git a/Plugins/ZShell/Internal/wallpaperobject.cpp b/Plugins/ZShell/Wallpaper/wallpaperobject.cpp similarity index 97% rename from Plugins/ZShell/Internal/wallpaperobject.cpp rename to Plugins/ZShell/Wallpaper/wallpaperobject.cpp index b29ce9c..143a637 100644 --- a/Plugins/ZShell/Internal/wallpaperobject.cpp +++ b/Plugins/ZShell/Wallpaper/wallpaperobject.cpp @@ -1,6 +1,6 @@ #include "wallpaperobject.hpp" -namespace ZShell::internal { +namespace ZShell::wallpaper { WallpaperObject::WallpaperObject(QObject* parent) : QObject(parent) {} @@ -118,4 +118,4 @@ void WallpaperObject::push() { m_service->setCrop(m_screen, crop); } -} // namespace ZShell::internal +} // namespace ZShell::wallpaper diff --git a/Plugins/ZShell/Internal/wallpaperobject.hpp b/Plugins/ZShell/Wallpaper/wallpaperobject.hpp similarity index 96% rename from Plugins/ZShell/Internal/wallpaperobject.hpp rename to Plugins/ZShell/Wallpaper/wallpaperobject.hpp index 2787ded..e2052be 100644 --- a/Plugins/ZShell/Internal/wallpaperobject.hpp +++ b/Plugins/ZShell/Wallpaper/wallpaperobject.hpp @@ -9,7 +9,7 @@ #include "wallpaperservice.hpp" -namespace ZShell::internal { +namespace ZShell::wallpaper { class WallpaperObject : public QObject { Q_OBJECT @@ -83,4 +83,4 @@ class WallpaperObject : public QObject { bool m_pushEnabled = true; }; -} // namespace ZShell::internal +} // namespace ZShell::wallpaper diff --git a/Plugins/ZShell/Internal/wallpaperservice.cpp b/Plugins/ZShell/Wallpaper/wallpaperservice.cpp similarity index 84% rename from Plugins/ZShell/Internal/wallpaperservice.cpp rename to Plugins/ZShell/Wallpaper/wallpaperservice.cpp index b8569a0..7ff181f 100644 --- a/Plugins/ZShell/Internal/wallpaperservice.cpp +++ b/Plugins/ZShell/Wallpaper/wallpaperservice.cpp @@ -46,25 +46,41 @@ #include "config/background.hpp" #include "config/rootnodes.hpp" +#include "config/services.hpp" #include -namespace ZShell::internal { +namespace ZShell::wallpaper { 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 + +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) @@ -213,6 +229,22 @@ WallpaperService::WallpaperService(QObject* parent) : QObject(parent) { 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() { @@ -260,11 +292,11 @@ WallpaperService::~WallpaperService() { if (m_captureTex) glDeleteTextures(1, &m_captureTex); if (m_captureFbo) glDeleteFramebuffers(1, &m_captureFbo); if (m_posterTex) glDeleteTextures(1, &m_posterTex); -#ifdef ZHAVE_VIDEO - if (m_player) { - m_player->stop(); - m_player->deleteTexture(); - m_player.reset(); +#ifdef ZHAVE_FFMPEG + if (m_decoder) { + m_decoder->stop(); + m_decoder->deleteTexture(); + m_decoder.reset(); } #endif } @@ -303,6 +335,8 @@ void WallpaperService::setDbPath(const QString& path) { m_forceDecode = true; } + m_cache.setDbPath(path); + emit dbPathChanged(); wake(); } @@ -310,8 +344,8 @@ void WallpaperService::setDbPath(const QString& 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 (isVideoPath(path) && m_transcoder->needsTranscode(path)) { + const QString dst = m_transcoder->transcodeTarget(path); if (QFile::exists(dst)) { applyPath(dst); @@ -319,7 +353,7 @@ void WallpaperService::setPath(const QString& path) { } applyPath(path); - transcodeVideo(path, dst); + m_transcoder->start(path, dst); return; } @@ -342,8 +376,8 @@ void WallpaperService::applyPath(const QString& path) { } } -#ifdef ZHAVE_VIDEO - const bool videoBackend = m_mpv != nullptr || m_player != nullptr; +#ifdef ZHAVE_FFMPEG + const bool videoBackend = m_mpv != nullptr || m_decoder != nullptr; #else const bool videoBackend = m_mpv != nullptr; #endif @@ -357,19 +391,19 @@ void WallpaperService::applyPath(const QString& path) { m_baseIsVideo = true; m_baseIsBlack = false; m_baseImage = QImage(); - m_baseCrops = cropsForPath(previousPath); + m_baseCrops = m_cache.cropsForPath(previousPath); } else if (wasImage) { m_needCaptureBase = true; m_baseIsVideo = false; m_baseIsBlack = false; m_baseImage = m_image; - m_baseCrops = cropsForPath(previousPath); + m_baseCrops = m_cache.cropsForPath(previousPath); } else { m_needCaptureBase = false; m_baseIsVideo = false; m_baseIsBlack = m_baseTex == 0; m_baseImage = QImage(); - m_baseCrops = cropsForPath(previousPath); + m_baseCrops = m_cache.cropsForPath(previousPath); } } @@ -383,7 +417,7 @@ void WallpaperService::applyPath(const QString& path) { TransitionLayer& newest = m_transitions.back(); newest.tex = m_texture; newest.image = m_image; - newest.crops = cropsForPath(previousPath); + newest.crops = m_cache.cropsForPath(previousPath); newest.ready = true; m_texture = 0; m_image = QImage(); @@ -393,7 +427,7 @@ void WallpaperService::applyPath(const QString& path) { m_transitions.back().isVideo) m_collapseVideo = true; - m_crops = cropsForPath(path); + m_crops = m_cache.cropsForPath(path); m_isVideo = video; @@ -424,153 +458,6 @@ void WallpaperService::applyPath(const QString& path) { 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); @@ -650,14 +537,14 @@ void WallpaperService::startVideo(const QString& path) { 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; +#ifdef ZHAVE_FFMPEG + if (m_useDecoder && !kMpvMode) { + if (startDecoderVideo(path)) return; qWarning(wpLog) << "WallpaperService: the GPU video player failed, " "falling back to mpv"; - m_usePlayer = false; - m_player.reset(); + m_useDecoder = false; + m_decoder.reset(); } #endif @@ -975,6 +862,16 @@ void WallpaperService::present() { 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) { @@ -991,16 +888,63 @@ void WallpaperService::present() { 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; } - if (!m_orphanTextures.empty()) { + { 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) || @@ -1016,19 +960,19 @@ void WallpaperService::present() { } else if (m_mpv != nullptr) { teardownMpv(); } -#ifdef ZHAVE_VIDEO - else if (m_player != nullptr) { - m_player->deleteTexture(); - stopPlayerVideo(); +#ifdef ZHAVE_FFMPEG + else if (m_decoder != nullptr) { + m_decoder->deleteTexture(); + stopDecoderVideo(); } #endif }; if (!holdVideoForBase) applyVideoBackendSwitch(); -#ifdef ZHAVE_VIDEO +#ifdef ZHAVE_FFMPEG const bool videoActive = - (m_mpv != nullptr || m_player != nullptr) && + (m_mpv != nullptr || m_decoder != nullptr) && (m_isVideo || m_baseIsVideo || m_collapseVideo || m_promoteOldest); #else const bool videoActive = @@ -1038,8 +982,8 @@ void WallpaperService::present() { if (videoActive && m_mpv != nullptr) pumpMpvEvents(); -#ifdef ZHAVE_VIDEO - if (videoActive && m_player != nullptr) m_player->importCurrentFrame(); +#ifdef ZHAVE_FFMPEG + if (videoActive && m_decoder != nullptr) m_decoder->importCurrentFrame(); #endif for (auto* surface : surfaces) @@ -1424,7 +1368,7 @@ void WallpaperService::ensureCapture(const QSize& size) { glBindFramebuffer(GL_FRAMEBUFFER, 0); } -#ifdef ZHAVE_VIDEO +#ifdef ZHAVE_FFMPEG uint32_t WallpaperService::createVideoProgram() { const QByteArray vsSrc = shaderSource("wallpaper.vert", false); const QByteArray fsSrc = shaderSource("wallpaper_video.frag", true); @@ -1518,30 +1462,32 @@ void WallpaperService::drawVideoTexture( glDrawArrays(GL_TRIANGLES, 0, 3); } -bool WallpaperService::startPlayerVideo(const QString& path) { - if (!m_player) m_player = std::make_unique(m_eglDisplay); +bool WallpaperService::startDecoderVideo(const QString& path) { + if (!m_decoder) + m_decoder = createVideoDecoder(m_eglDisplay, backendFromConfig()); + if (!m_decoder) return false; - m_player->setReadyCallback([this]() { + m_decoder->setReadyCallback([this]() { m_videoLoaded.store(true, std::memory_order_relaxed); m_videoReady.store(true, std::memory_order_relaxed); queueAll(); }); - m_player->setFrameCallback([this]() { + m_decoder->setFrameCallback([this]() { m_videoReady.store(true, std::memory_order_relaxed); queueAll(); }); - if (!m_player->start(path)) return false; + if (!m_decoder->start(path)) return false; return true; } -void WallpaperService::stopPlayerVideo() { - if (!m_player) return; +void WallpaperService::stopDecoderVideo() { + if (!m_decoder) return; - m_player->stop(); - m_player.reset(); + m_decoder->stop(); + m_decoder.reset(); m_videoReady.store(false, std::memory_order_relaxed); m_videoLoaded.store(false, std::memory_order_relaxed); @@ -1556,19 +1502,19 @@ uint32_t WallpaperService::renderVideoIntoTexture(const QSize& size) { glViewport(0, 0, size.width(), size.height()); glDisable(GL_BLEND); -#ifdef ZHAVE_VIDEO - if (m_player) { - if (m_player->textureId() != 0) { +#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_player->frameSize(); + const QSize src = m_decoder->frameSize(); drawVideoTexture( - m_player->textureId(), - m_player->chromaTextureId(), - m_player->colorMatrix(), - m_player->colorRange(), + 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()), @@ -1659,193 +1605,6 @@ void WallpaperService::drawTexture( 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; -} - -QByteArray WallpaperService::posterBlob(const QString& path) const { - if (path.isEmpty()) return {}; - - QString databasePath; - { - std::lock_guard lock(m_mutex); - databasePath = m_dbPath; - } - - if (databasePath.isEmpty()) { - qWarning(wpLog) << "WallpaperService: poster lookup, no database path"; - return {}; - } - - const QFileInfo info(path); - - const QString connection = QStringLiteral("wallpaper-cache-%1") - .arg(quintptr(QThread::currentThreadId())); - - QSqlDatabase database = - QSqlDatabase::contains(connection) - ? QSqlDatabase::database(connection, false) - : QSqlDatabase::addDatabase("QSQLITE", connection); - - if (database.databaseName() != databasePath) { - if (database.isOpen()) database.close(); - database.setDatabaseName(databasePath); - } - - if (!database.isOpen() && !database.open()) { - qWarning(wpLog) - << "WallpaperService: poster lookup, database could not open"; - return {}; - } - - QSqlQuery query(database); - query.prepare("SELECT poster, size, mtime FROM wallpapers WHERE path = ?"); - query.addBindValue(info.absoluteFilePath()); - if (!query.exec() || !query.next()) { - qWarning(wpLog) << "WallpaperService: poster lookup, no row for" << path - << query.lastError().text(); - return {}; - } - - const QByteArray blob = query.value(0).toByteArray(); - if (blob.isEmpty()) { - qWarning(wpLog) << "WallpaperService: poster column empty for" << path; - return {}; - } - - const qint64 mtime = info.lastModified().toMSecsSinceEpoch() / 1000; - if (query.value(1).toLongLong() != info.size() || - query.value(2).toLongLong() != mtime) { - qWarning(wpLog) << "WallpaperService: poster stale for" << path; - return {}; - } - - return blob; -} - -QVariantMap WallpaperService::cropsForPath(const QString& path) const { - if (path.isEmpty()) return {}; - - QString databasePath; - { - std::lock_guard lock(m_mutex); - databasePath = m_dbPath; - } - - if (databasePath.isEmpty()) return {}; - - const QFileInfo info(path); - - const QString connection = QStringLiteral("wallpaper-cache-%1") - .arg(quintptr(QThread::currentThreadId())); - - QSqlDatabase database = - QSqlDatabase::contains(connection) - ? QSqlDatabase::database(connection, false) - : QSqlDatabase::addDatabase("QSQLITE", connection); - - if (database.databaseName() != databasePath) { - if (database.isOpen()) database.close(); - database.setDatabaseName(databasePath); - } - - if (!database.isOpen() && !database.open()) return {}; - - QSqlQuery query(database); - query.prepare("SELECT crop FROM wallpapers WHERE path = ?"); - query.addBindValue(info.absoluteFilePath()); - if (!query.exec() || !query.next()) return {}; - - const QByteArray json = query.value(0).toByteArray(); - if (json.isEmpty()) return {}; - - const auto doc = QJsonDocument::fromJson(json); - if (!doc.isObject()) return {}; - - return doc.object().toVariantMap(); -} - -QImage WallpaperService::decodeImage( - const QString& path, const QSize& decodeSize) const { - if (path.isEmpty()) return {}; - - QImageReader probe(path); - const QSize source = probe.size(); - QSize required = source.isEmpty() ? decodeSize : source; - if (!source.isEmpty() && !decodeSize.isEmpty()) { - const QSize cover = - source.scaled(decodeSize, Qt::KeepAspectRatioByExpanding); - if (cover.width() <= source.width() && - cover.height() <= source.height()) - required = cover; - } - - const QByteArray blob = cachedBlob(path); - if (!blob.isEmpty()) { - QImage image = QImage::fromData(blob, "JPEG"); - if (!image.isNull() && image.width() >= required.width() && - image.height() >= required.height()) { - return image.convertToFormat(QImage::Format_RGBA8888); - } - } - - QImageReader reader(path); - reader.setAutoTransform(true); - - if (!decodeSize.isEmpty() && !source.isEmpty()) - reader.setScaledSize( - source.scaled(decodeSize, Qt::KeepAspectRatioByExpanding)); - - QImage image = reader.read(); - if (image.isNull()) { - qWarning(wpLog) << "WallpaperService: could not read the image" << path; - return {}; - } - - return image.convertToFormat(QImage::Format_RGBA8888); -} - void WallpaperService::ensureImage() { QSize largest; QString path; @@ -1891,7 +1650,7 @@ void WallpaperService::ensureImage() { if (!needDecode) return; QThreadPool::globalInstance()->start([this, path, largest]() { - const QImage image = decodeImage(path, largest); + const QImage image = m_cache.decodeImage(path, largest); std::lock_guard lock(m_mutex); m_stagedImage = image; @@ -1923,7 +1682,7 @@ void WallpaperService::ensurePoster() { } QThreadPool::globalInstance()->start([this, path]() { - const QByteArray blob = posterBlob(path); + const QByteArray blob = m_cache.posterBlob(path); QImage image; if (!blob.isEmpty()) { @@ -2074,9 +1833,9 @@ void WallpaperService::drawSurface(WallpaperSurface* surface) { if (videoNow && !m_videoReady.load(std::memory_order_relaxed)) return; -#ifdef ZHAVE_VIDEO +#ifdef ZHAVE_FFMPEG const bool videoAsBackground = - videoNow && (m_mpv != nullptr || m_player != nullptr); + videoNow && (m_mpv != nullptr || m_decoder != nullptr); #else const bool videoAsBackground = videoNow && m_mpv != nullptr; #endif @@ -2085,20 +1844,20 @@ void WallpaperService::drawSurface(WallpaperSurface* surface) { glDisable(GL_BLEND); glViewport(0, 0, size.width(), size.height()); -#ifdef ZHAVE_VIDEO - if (m_player) { - if (m_player->textureId() != 0) { +#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_player->frameSize(); + const QSize src = m_decoder->frameSize(); drawVideoTexture( - m_player->textureId(), - m_player->chromaTextureId(), - m_player->colorMatrix(), - m_player->colorRange(), + 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()), @@ -2506,7 +2265,7 @@ void WallpaperService::drawSurfaceTransition(WallpaperSurface* surface) { oldTex = videoTex; oldFlip = 0.0f; } else { - oldTex = m_texture; // still the previous image here + oldTex = m_texture; oldFlip = 1.0f; oldCrop = sourceRect(m_baseCrops, screen, size, m_baseImage.size()); @@ -2622,20 +2381,20 @@ void WallpaperService::drawSurfaceTransition(WallpaperSurface* surface) { } else { glDisable(GL_BLEND); -#ifdef ZHAVE_VIDEO - if (m_player) { - if (m_player->textureId() != 0) { +#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_player->frameSize(); + const QSize src = m_decoder->frameSize(); drawVideoTexture( - m_player->textureId(), - m_player->chromaTextureId(), - m_player->colorMatrix(), - m_player->colorRange(), + m_decoder->textureId(), + m_decoder->chromaTextureId(), + m_decoder->colorMatrix(), + m_decoder->colorRange(), QRectF(0.0, 0.0, 1.0, 1.0), w, h, @@ -2753,4 +2512,4 @@ void WallpaperService::addSurface(const QString& screen) { notifyBufferSizes(); } -} // namespace ZShell::internal +} // namespace ZShell::wallpaper diff --git a/Plugins/ZShell/Internal/wallpaperservice.hpp b/Plugins/ZShell/Wallpaper/wallpaperservice.hpp similarity index 75% rename from Plugins/ZShell/Internal/wallpaperservice.hpp rename to Plugins/ZShell/Wallpaper/wallpaperservice.hpp index dad41d8..60c8073 100644 --- a/Plugins/ZShell/Internal/wallpaperservice.hpp +++ b/Plugins/ZShell/Wallpaper/wallpaperservice.hpp @@ -22,9 +22,9 @@ #include #include -#ifdef ZHAVE_VIDEO -#include "videoplayer.hpp" -#endif +#include "videodecoder.hpp" +#include "wallpapercache.hpp" +#include "videotranscoder.hpp" #include #include @@ -37,7 +37,7 @@ struct mpv_render_context; class QProcess; -namespace ZShell::internal { +namespace ZShell::wallpaper { class WallpaperSurface; @@ -87,14 +87,6 @@ class WallpaperService : public QObject { static bool isVideoPath(const QString& path); - static bool isHardwareCodec(const QString& codec); - - [[nodiscard]] QString codecName(const QString& path) const; - [[nodiscard]] QString cacheDir() const; - [[nodiscard]] QString transcodeTarget(const QString& path) const; - [[nodiscard]] bool needsTranscode(const QString& path) const; - void transcodeVideo(const QString& src, const QString& dst); - void applyPath(const QString& path); void startVideo(const QString& path); @@ -107,9 +99,9 @@ class WallpaperService : public QObject { static void* getMpvProcAddress(void* ctx, const char* name); static void mpvUpdate(void* ctx); -#ifdef ZHAVE_VIDEO - bool startPlayerVideo(const QString& path); - void stopPlayerVideo(); +#ifdef ZHAVE_FFMPEG + bool startDecoderVideo(const QString& path); + void stopDecoderVideo(); [[nodiscard]] uint32_t createVideoProgram(); void drawVideoTexture( uint32_t lumaTex, @@ -160,13 +152,13 @@ class WallpaperService : public QObject { void drawSurfaceTransition(WallpaperSurface* surface); struct TransitionLayer { - uint32_t tex = 0; // frozen image target (older layers) - QImage image; // CPU copy for crops/sizing (image layers) - QVariantMap crops; // crop map captured when the layer was pushed - QString transitionFile; // this layer's own transition shader - bool isVideo = false; // only the newest layer may be video - bool ready = false; // target is sampleable - bool started = false; // its own clock has begun + uint32_t tex = 0; + QImage image; + QVariantMap crops; + QString transitionFile; + bool isVideo = false; + bool ready = false; + bool started = false; QElapsedTimer timer; qreal progress = 0.0; }; @@ -216,15 +208,6 @@ class WallpaperService : public QObject { float sizeH, float alpha); - [[nodiscard]] QImage decodeImage( - const QString& path, const QSize& decodeSize) const; - - [[nodiscard]] QByteArray cachedBlob(const QString& path) const; - - [[nodiscard]] QByteArray posterBlob(const QString& path) const; - - [[nodiscard]] QVariantMap cropsForPath(const QString& path) const; - void ensureImage(); void ensurePoster(); @@ -260,6 +243,8 @@ class WallpaperService : public QObject { QStringList m_screens; QVariantMap m_crops; + WallpaperCache m_cache; + QImage m_image; bool m_isVideo = false; @@ -271,7 +256,7 @@ class WallpaperService : public QObject { std::atomic m_videoReady{false}; std::atomic m_videoLoaded{false}; - QProcess* m_transcode = nullptr; + VideoTranscoder* m_transcoder = nullptr; QSize m_decodeSize; @@ -299,16 +284,13 @@ class WallpaperService : public QObject { int m_alphaUniform = -1; int m_sourceUniform = -1; - bool m_randomTransition = false; // wallTransition == Random - QString m_transitionFile; // the transition stamped onto new layers + bool m_randomTransition = false; + QString m_transitionFile; bool m_transitionDirty = false; - QHash - m_transPrograms; // compiled programs, keyed by shader - TransProgram - m_neutralProgram; // fade program for identity copies (seed/base/final) - QVector - m_transitionUses; // usage counts, parallel to the transition list - QString m_lastTransition; // last random pick, so it is never repeated + QHash m_transPrograms; + TransProgram m_neutralProgram; + QVector m_transitionUses; + QString m_lastTransition; uint32_t m_captureFbo = 0; uint32_t m_captureTex = 0; @@ -324,17 +306,16 @@ class WallpaperService : public QObject { uint32_t m_accumB = 0; QSize m_accumSize; - std::vector m_transitions; // oldest .. newest + std::vector m_transitions; bool m_needCaptureBase = false; - QImage m_baseImage; // old settled image, for the base capture crop - QVariantMap m_baseCrops; // old settled crop map, for the base capture crop - bool m_baseIsVideo = false; // old settled frame was a live video - bool m_baseIsBlack = false; // shell start / first load: fade in from black - bool m_collapseVideo = - false; // video pushed mid-stack: bake older into base - bool m_promoteOldest = false; // stack over the cap: promote oldest to base - bool m_newestDone = false; // newest layer finished: settle into base + QImage m_baseImage; + QVariantMap m_baseCrops; + bool m_baseIsVideo = false; + bool m_baseIsBlack = false; + bool m_collapseVideo = false; + bool m_promoteOldest = false; + bool m_newestDone = false; static constexpr int kMaxTransitions = 4; @@ -350,13 +331,15 @@ class WallpaperService : public QObject { int m_videoRangeUniform = -1; int m_videoChromaOffUniform = -1; -#ifdef ZHAVE_VIDEO - std::unique_ptr m_player; - bool m_usePlayer = true; +#ifdef ZHAVE_FFMPEG + std::unique_ptr m_decoder; + bool m_useDecoder = true; #endif std::vector m_orphanTextures; + bool m_programsCompiled = false; + QList m_surfaces; std::thread m_thread; @@ -381,4 +364,4 @@ class WallpaperService : public QObject { bool m_posterChanged = false; }; -} // namespace ZShell::internal +} // namespace ZShell::wallpaper diff --git a/Plugins/ZShell/Internal/wallpapersurface.cpp b/Plugins/ZShell/Wallpaper/wallpapersurface.cpp similarity index 98% rename from Plugins/ZShell/Internal/wallpapersurface.cpp rename to Plugins/ZShell/Wallpaper/wallpapersurface.cpp index 1fb7834..551b610 100644 --- a/Plugins/ZShell/Internal/wallpapersurface.cpp +++ b/Plugins/ZShell/Wallpaper/wallpapersurface.cpp @@ -6,7 +6,7 @@ #include "wallpaperservice.hpp" -namespace ZShell::internal { +namespace ZShell::wallpaper { const QString kNamespace = "ZShell-Wallpaper"; @@ -188,4 +188,4 @@ void WallpaperSurface::updateBuffer() { m_service->notifyBufferSizes(); } -} // namespace ZShell::internal +} // namespace ZShell::wallpaper diff --git a/Plugins/ZShell/Internal/wallpapersurface.hpp b/Plugins/ZShell/Wallpaper/wallpapersurface.hpp similarity index 96% rename from Plugins/ZShell/Internal/wallpapersurface.hpp rename to Plugins/ZShell/Wallpaper/wallpapersurface.hpp index bada4ee..2100023 100644 --- a/Plugins/ZShell/Internal/wallpapersurface.hpp +++ b/Plugins/ZShell/Wallpaper/wallpapersurface.hpp @@ -13,7 +13,7 @@ #include #include -namespace ZShell::internal { +namespace ZShell::wallpaper { class WallpaperService; @@ -70,4 +70,4 @@ class WallpaperSurface : public QtWayland::zwlr_layer_surface_v1, struct wl_callback* m_frameCallback = nullptr; }; -} // namespace ZShell::internal +} // namespace ZShell::wallpaper