feat: add vaapi path for video wallpapers on generic gpus
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 11s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 32s
Rust / build (pull_request) Successful in 52s
Python / verify (pull_request) Failing after 1m23s
Rust / fmt (pull_request) Successful in 51s
Rust / clippy (pull_request) Successful in 1m2s

This commit is contained in:
2026-10-27 02:15:16 +01:00
parent e655ec5bc7
commit b0cb43f75c
39 changed files with 1670 additions and 599 deletions
+1
View File
@@ -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)
+1
View File
@@ -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)
-41
View File
@@ -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()
+1 -1
View File
@@ -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 {
+70
View File
@@ -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
)
@@ -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<std::mutex> 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<void()> 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<void()> 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<std::mutex> lock(m_mtx);
CudaCtx ctxGuard(m_cuCtx);
closeDecoder();
std::lock_guard<std::mutex> 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<std::mutex> lock(m_mtx);
return m_size;
}
void VideoPlayer::deleteTexture() {
void NvidiaPlayer::deleteTexture() {
std::lock_guard<std::mutex> 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<std::mutex> lock(m_mtx);
@@ -546,4 +562,6 @@ bool VideoPlayer::importCurrentFrame() {
return true;
}
} // namespace ZShell::wallpaper
#endif
@@ -1,11 +1,14 @@
#pragma once
#ifdef ZHAVE_VIDEO
#include "videodecoder.hpp"
#include <EGL/egl.h>
#include <cuda.h>
#include <atomic>
#include <functional>
#include <mutex>
#include <thread>
@@ -23,27 +26,21 @@ extern "C" {
#include <libavutil/pixdesc.h>
}
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<void()> callback);
void setFrameCallback(std::function<void()> 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<void()> m_readyCb;
std::function<void()> m_frameCb;
std::thread m_thread;
std::atomic<bool> m_threadRunning{false};
std::atomic<uint32_t> 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
+628
View File
@@ -0,0 +1,628 @@
#define EGL_EGLEXT_PROTOTYPES
#include "vaapiplayer.hpp"
#ifdef ZHAVE_FFMPEG
#define GL_GLEXT_PROTOTYPES
#include <EGL/egl.h>
#include <GL/gl.h>
#include <GL/glext.h>
#ifdef ZHAVE_VAAPI
#include <unistd.h>
extern "C" {
#include <va/va.h>
#include <va/va_drmcommon.h>
#include <libavutil/hwcontext_vaapi.h>
}
#ifndef DRM_FORMAT_MOD_INVALID
#define DRM_FORMAT_MOD_INVALID 0xffffffffffffffffULL
#endif
#endif
#include <algorithm>
#include <chrono>
#include <climits>
#include <QtLogging>
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<GLEGLImageTargetProc>(
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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<const AVHWFramesContext*>(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<std::mutex> 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<std::mutex> 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<const AVHWFramesContext*>(frame->hw_frames_ctx->data);
const AVHWDeviceContext* dev = hf->device_ctx;
const AVVAAPIDeviceContext* va =
reinterpret_cast<const AVVAAPIDeviceContext*>(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
+83
View File
@@ -0,0 +1,83 @@
#pragma once
#include "videodecoder.hpp"
#include <EGL/egl.h>
#include <atomic>
#include <mutex>
#include <thread>
#include <QSize>
#include <QString>
#include <EGL/egl.h>
#ifdef ZHAVE_VAAPI
#include <EGL/eglext.h>
#endif
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/buffer.h>
#include <libavutil/frame.h>
#include <libavutil/hwcontext.h>
#include <libavutil/pixfmt.h>
#include <libavutil/pixdesc.h>
}
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<bool> m_threadRunning{false};
std::atomic<uint32_t> 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
+117
View File
@@ -0,0 +1,117 @@
#include "videodecoder.hpp"
#include <QtGlobal>
#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<void()> callback) {
m_readyCb = std::move(callback);
}
void VideoDecoder::setFrameCallback(std::function<void()> 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<VideoDecoder> createVideoDecoder(
EGLDisplay display, VideoBackend hint) {
switch (selectBackend(hint)) {
#ifdef ZHAVE_VIDEO
case VideoBackend::Nvidia:
return std::make_unique<NvidiaPlayer>(display);
#endif
#ifdef ZHAVE_FFMPEG
case VideoBackend::Vaapi:
return std::make_unique<VaapiPlayer>(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
+58
View File
@@ -0,0 +1,58 @@
#pragma once
#include <EGL/egl.h>
#include <cstdint>
#include <functional>
#include <memory>
#include <QSize>
#include <QString>
namespace ZShell::wallpaper {
class VideoDecoder {
public:
virtual ~VideoDecoder();
void setReadyCallback(std::function<void()> callback);
void setFrameCallback(std::function<void()> 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<void()> m_readyCb;
std::function<void()> 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<VideoDecoder> createVideoDecoder(
EGLDisplay display, VideoBackend hint = VideoBackend::Auto);
const char* resolveBackendName(VideoBackend hint = VideoBackend::Auto);
} // namespace ZShell::wallpaper
@@ -0,0 +1,165 @@
#include "videotranscoder.hpp"
#include <QCryptographicHash>
#include <QDir>
#include <QFileInfo>
#include <QFile>
#include <QProcess>
#include <QStringList>
#include <QtLogging>
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
@@ -0,0 +1,33 @@
#pragma once
#include <QObject>
#include <QString>
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
+170
View File
@@ -0,0 +1,170 @@
#include "wallpapercache.hpp"
#include <QCryptographicHash>
#include <QDir>
#include <QFileInfo>
#include <QImageReader>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QThread>
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<std::mutex> lock(m_mtx);
m_dbPath = path;
}
QString WallpaperCache::dbPath() const {
std::lock_guard<std::mutex> 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
@@ -0,0 +1,33 @@
#pragma once
#include <QByteArray>
#include <QByteArray>
#include <QImage>
#include <QSize>
#include <QString>
#include <QVariantMap>
#include <mutex>
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
@@ -8,7 +8,7 @@
#include <QtConcurrent>
#include <QQuickWindow>
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
@@ -14,7 +14,7 @@
#include <qtmetamacros.h>
#include <atomic>
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
@@ -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
@@ -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
@@ -46,25 +46,41 @@
#include "config/background.hpp"
#include "config/rootnodes.hpp"
#include "config/services.hpp"
#include <cstring>
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<std::recursive_mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> lock(m_mutex);
m_videoReload = false;
}
if (!m_orphanTextures.empty()) {
{
std::lock_guard<std::recursive_mutex> 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<VideoPlayer>(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<std::recursive_mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> 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
@@ -22,9 +22,9 @@
#include <atomic>
#include <memory>
#ifdef ZHAVE_VIDEO
#include "videoplayer.hpp"
#endif
#include "videodecoder.hpp"
#include "wallpapercache.hpp"
#include "videotranscoder.hpp"
#include <qwayland-wlr-layer-shell-unstable-v1.h>
#include <wayland-client-protocol.h>
@@ -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<bool> m_videoReady{false};
std::atomic<bool> 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<QString, TransProgram>
m_transPrograms; // compiled programs, keyed by shader
TransProgram
m_neutralProgram; // fade program for identity copies (seed/base/final)
QVector<int>
m_transitionUses; // usage counts, parallel to the transition list
QString m_lastTransition; // last random pick, so it is never repeated
QHash<QString, TransProgram> m_transPrograms;
TransProgram m_neutralProgram;
QVector<int> 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<TransitionLayer> m_transitions; // oldest .. newest
std::vector<TransitionLayer> 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<VideoPlayer> m_player;
bool m_usePlayer = true;
#ifdef ZHAVE_FFMPEG
std::unique_ptr<VideoDecoder> m_decoder;
bool m_useDecoder = true;
#endif
std::vector<uint32_t> m_orphanTextures;
bool m_programsCompiled = false;
QList<WallpaperSurface*> m_surfaces;
std::thread m_thread;
@@ -381,4 +364,4 @@ class WallpaperService : public QObject {
bool m_posterChanged = false;
};
} // namespace ZShell::internal
} // namespace ZShell::wallpaper
@@ -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
@@ -13,7 +13,7 @@
#include <qwayland-wlr-layer-shell-unstable-v1.h>
#include <wayland-client-protocol.h>
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