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
2516 lines
62 KiB
C++
2516 lines
62 KiB
C++
#define EGL_EGLEXT_PROTOTYPES
|
|
#define GL_GLEXT_PROTOTYPES
|
|
|
|
#include "wallpaperservice.hpp"
|
|
|
|
#include <EGL/eglext.h>
|
|
#include <GL/gl.h>
|
|
#include <GL/glext.h>
|
|
|
|
#ifndef GL_TEXTURE_EXTERNAL_OES
|
|
#define GL_TEXTURE_EXTERNAL_OES 0x8D66
|
|
#endif
|
|
|
|
#include <mpv/client.h>
|
|
#include <mpv/render_gl.h>
|
|
|
|
#include <clocale>
|
|
|
|
#include <fcntl.h>
|
|
#include <poll.h>
|
|
#include <unistd.h>
|
|
|
|
#include <QDebug>
|
|
#include <QElapsedTimer>
|
|
#include <QFileInfo>
|
|
#include <QGuiApplication>
|
|
#include <QImageReader>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QRandomGenerator>
|
|
#include <QScreen>
|
|
#include <QSqlDatabase>
|
|
#include <QSqlError>
|
|
#include <QSqlQuery>
|
|
#include <QThread>
|
|
#include <QThreadPool>
|
|
|
|
#include <QCryptographicHash>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QProcess>
|
|
|
|
#include <qguiapplication_platform.h>
|
|
|
|
#include <private/qwaylandscreen_p.h>
|
|
|
|
#include "config/background.hpp"
|
|
#include "config/rootnodes.hpp"
|
|
#include "config/services.hpp"
|
|
|
|
#include <cstring>
|
|
|
|
namespace ZShell::wallpaper {
|
|
|
|
Q_LOGGING_CATEGORY(wpLog, "ZShell.Internal.wallpaper")
|
|
|
|
namespace {
|
|
|
|
const bool kMpvMode = qEnvironmentVariableIsSet("ZBAR_WALLPAPER_MPV");
|
|
|
|
const bool kGlesMode = qEnvironmentVariableIsSet("ZBAR_WALLPAPER_GLES") ||
|
|
kMpvMode;
|
|
|
|
VideoBackend backendFromConfig() {
|
|
const auto type =
|
|
ZShell::config::ConfigSingleton::instance()->services()->gpuType();
|
|
switch (type) {
|
|
case ZShell::config::GpuType::Nvidia:
|
|
return VideoBackend::Nvidia;
|
|
case ZShell::config::GpuType::Generic:
|
|
return VideoBackend::Vaapi;
|
|
case ZShell::config::GpuType::None:
|
|
return VideoBackend::None;
|
|
case ZShell::config::GpuType::Auto:
|
|
default:
|
|
return VideoBackend::Auto;
|
|
}
|
|
}
|
|
|
|
void logSelectedBackend() {
|
|
qInfo(wpLog) << "WallpaperService: video backend ="
|
|
<< resolveBackendName(backendFromConfig());
|
|
}
|
|
|
|
const char* shaderHeader(bool fragment) {
|
|
if (kGlesMode)
|
|
return fragment ? "#version 300 es\nprecision highp float;\n"
|
|
: "#version 300 es\n";
|
|
return "#version 330\n";
|
|
}
|
|
|
|
QByteArray shaderSource(const QString& file, bool fragment) {
|
|
QFile source(QStringLiteral(":/shaders/") + file);
|
|
if (!source.open(QIODevice::ReadOnly)) {
|
|
qWarning(wpLog) << "WallpaperService: could not read the shader"
|
|
<< file;
|
|
return {};
|
|
}
|
|
|
|
return QByteArray(shaderHeader(fragment)) + source.readAll();
|
|
}
|
|
|
|
const char* transitionFile(ZShell::config::WallpaperTransition::Enum value) {
|
|
switch (value) {
|
|
case ZShell::config::WallpaperTransition::Bowtie:
|
|
return "wallpaper_trans_bowtie.frag";
|
|
case ZShell::config::WallpaperTransition::Fade:
|
|
return "wallpaper_trans_fade.frag";
|
|
case ZShell::config::WallpaperTransition::LinearBlur:
|
|
return "wallpaper_trans_linear_blur.frag";
|
|
case ZShell::config::WallpaperTransition::WindowSlices:
|
|
return "wallpaper_trans_window_slices.frag";
|
|
case ZShell::config::WallpaperTransition::WipeRight:
|
|
return "wallpaper_trans_wipe_right.frag";
|
|
case ZShell::config::WallpaperTransition::WipeDown:
|
|
return "wallpaper_trans_wipe_down.frag";
|
|
case ZShell::config::WallpaperTransition::TilesWave:
|
|
return "wallpaper_trans_tiles_wave.frag";
|
|
case ZShell::config::WallpaperTransition::SplitSlideInHorizontal:
|
|
return "wallpaper_trans_split_slide_in_horizontal.frag";
|
|
case ZShell::config::WallpaperTransition::Hexagonalize:
|
|
return "wallpaper_trans_hexagonalize.frag";
|
|
case ZShell::config::WallpaperTransition::DoomMelt:
|
|
return "wallpaper_trans_doom_melt.frag";
|
|
case ZShell::config::WallpaperTransition::Random:
|
|
break;
|
|
}
|
|
|
|
return "wallpaper_trans_fade.frag";
|
|
}
|
|
|
|
const QStringList& transitionFilesList() {
|
|
static const QStringList files = {
|
|
"wallpaper_trans_bowtie.frag",
|
|
"wallpaper_trans_fade.frag",
|
|
"wallpaper_trans_linear_blur.frag",
|
|
"wallpaper_trans_window_slices.frag",
|
|
"wallpaper_trans_wipe_right.frag",
|
|
"wallpaper_trans_wipe_down.frag",
|
|
"wallpaper_trans_tiles_wave.frag",
|
|
"wallpaper_trans_split_slide_in_horizontal.frag",
|
|
"wallpaper_trans_hexagonalize.frag",
|
|
"wallpaper_trans_doom_melt.frag",
|
|
};
|
|
return files;
|
|
}
|
|
|
|
constexpr int kWaitTimeoutMs = 20;
|
|
|
|
constexpr int kFadeDurationMs = 400;
|
|
|
|
const QStringList kVideoExtensions = {
|
|
"mp4",
|
|
"mkv",
|
|
"webm",
|
|
"avi",
|
|
"mov",
|
|
"wmv",
|
|
"flv",
|
|
"m4v",
|
|
"mpg",
|
|
"mpeg",
|
|
"3gp",
|
|
"ogv",
|
|
"ts",
|
|
"vob",
|
|
};
|
|
|
|
void logShader(uint32_t object, bool program) {
|
|
int ok = 0;
|
|
if (program)
|
|
glGetProgramiv(object, GL_LINK_STATUS, &ok);
|
|
else
|
|
glGetShaderiv(object, GL_COMPILE_STATUS, &ok);
|
|
|
|
if (ok) return;
|
|
|
|
char info[512] = {};
|
|
if (program)
|
|
glGetProgramInfoLog(object, int(sizeof(info)), nullptr, info);
|
|
else
|
|
glGetShaderInfoLog(object, int(sizeof(info)), nullptr, info);
|
|
|
|
qWarning(wpLog) << "WallpaperService:"
|
|
<< (program ? "linking" : "compiling") << "failed:" << info;
|
|
}
|
|
|
|
QVariantMap cropFor(const QVariantMap& crops, const QString& screen) {
|
|
QVariantMap crop = crops.value(screen).toMap();
|
|
|
|
if (crop.isEmpty()) {
|
|
crop.insert("x", 0.0);
|
|
crop.insert("y", 0.0);
|
|
crop.insert("width", 1.0);
|
|
crop.insert("height", 1.0);
|
|
crop.insert("zoom", 1.0);
|
|
}
|
|
|
|
return crop;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
WallpaperService::WallpaperService(QObject* parent) : QObject(parent) {
|
|
auto* background =
|
|
ZShell::config::ConfigSingleton::instance()->background();
|
|
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
m_randomTransition = background->wallTransition() ==
|
|
ZShell::config::WallpaperTransition::Random;
|
|
m_transitionFile = transitionFileFor(background->wallTransition());
|
|
}
|
|
|
|
QObject::connect(
|
|
background,
|
|
&ZShell::config::Background::wallTransitionChanged,
|
|
this,
|
|
[this, background] {
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
m_randomTransition =
|
|
background->wallTransition() ==
|
|
ZShell::config::WallpaperTransition::Random;
|
|
m_transitionFile =
|
|
transitionFileFor(background->wallTransition());
|
|
m_transitionDirty = true;
|
|
}
|
|
|
|
wake();
|
|
});
|
|
|
|
m_transcoder = new VideoTranscoder(this);
|
|
connect(
|
|
m_transcoder,
|
|
&VideoTranscoder::transcoded,
|
|
this,
|
|
[this](const QString& dst, bool ok) {
|
|
if (ok) applyPath(dst);
|
|
});
|
|
|
|
auto* gpuConfig = ZShell::config::ConfigSingleton::instance()->services();
|
|
logSelectedBackend();
|
|
QObject::connect(
|
|
gpuConfig, &ZShell::config::Services::gpuTypeChanged, this, [] {
|
|
logSelectedBackend();
|
|
});
|
|
}
|
|
|
|
WallpaperService::~WallpaperService() {
|
|
m_stop = true;
|
|
wake();
|
|
|
|
if (m_thread.joinable()) m_thread.join();
|
|
|
|
if (m_mpvRender) {
|
|
mpv_render_context_free(m_mpvRender);
|
|
m_mpvRender = nullptr;
|
|
}
|
|
|
|
if (m_mpv) {
|
|
mpv_terminate_destroy(m_mpv);
|
|
m_mpv = nullptr;
|
|
}
|
|
|
|
for (auto* surface : m_surfaces)
|
|
delete surface;
|
|
m_surfaces.clear();
|
|
|
|
if (m_eglDisplay != EGL_NO_DISPLAY && m_context != EGL_NO_CONTEXT) {
|
|
if (eglMakeCurrent(
|
|
m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, m_context)) {
|
|
if (m_texture) glDeleteTextures(1, &m_texture);
|
|
for (const auto& layer : m_transitions)
|
|
if (layer.tex) glDeleteTextures(1, &layer.tex);
|
|
m_transitions.clear();
|
|
if (m_baseTex) glDeleteTextures(1, &m_baseTex);
|
|
if (m_baseFbo) glDeleteFramebuffers(1, &m_baseFbo);
|
|
if (m_accumA) glDeleteTextures(1, &m_accumA);
|
|
if (m_accumB) glDeleteTextures(1, &m_accumB);
|
|
if (m_accumFbo) glDeleteFramebuffers(1, &m_accumFbo);
|
|
for (uint32_t tex : m_orphanTextures)
|
|
glDeleteTextures(1, &tex);
|
|
m_orphanTextures.clear();
|
|
if (m_blackTex) glDeleteTextures(1, &m_blackTex);
|
|
if (m_program) glDeleteProgram(m_program);
|
|
if (m_videoProgram) glDeleteProgram(m_videoProgram);
|
|
for (const auto& p : m_transPrograms)
|
|
if (p.id) glDeleteProgram(p.id);
|
|
m_transPrograms.clear();
|
|
m_neutralProgram = TransProgram();
|
|
if (m_captureTex) glDeleteTextures(1, &m_captureTex);
|
|
if (m_captureFbo) glDeleteFramebuffers(1, &m_captureFbo);
|
|
if (m_posterTex) glDeleteTextures(1, &m_posterTex);
|
|
#ifdef ZHAVE_FFMPEG
|
|
if (m_decoder) {
|
|
m_decoder->stop();
|
|
m_decoder->deleteTexture();
|
|
m_decoder.reset();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
eglDestroyContext(m_eglDisplay, m_context);
|
|
}
|
|
|
|
if (m_queue) wl_event_queue_destroy(m_queue);
|
|
|
|
if (m_wake[0] >= 0) {
|
|
close(m_wake[0]);
|
|
close(m_wake[1]);
|
|
}
|
|
|
|
if (m_layerShell) m_layerShell->destroy();
|
|
if (m_viewporter) m_viewporter->destroy();
|
|
if (m_scaleManager) m_scaleManager->destroy();
|
|
if (m_registry) wl_registry_destroy(m_registry);
|
|
}
|
|
|
|
QString WallpaperService::path() const {
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
return m_path;
|
|
}
|
|
|
|
QString WallpaperService::dbPath() const {
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
return m_dbPath;
|
|
}
|
|
|
|
void WallpaperService::setDbPath(const QString& path) {
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
if (m_dbPath == path) return;
|
|
m_dbPath = path;
|
|
m_forceDecode = true;
|
|
}
|
|
|
|
m_cache.setDbPath(path);
|
|
|
|
emit dbPathChanged();
|
|
wake();
|
|
}
|
|
|
|
void WallpaperService::setPath(const QString& path) {
|
|
if (path.isEmpty() || path == this->path()) return;
|
|
|
|
if (isVideoPath(path) && m_transcoder->needsTranscode(path)) {
|
|
const QString dst = m_transcoder->transcodeTarget(path);
|
|
|
|
if (QFile::exists(dst)) {
|
|
applyPath(dst);
|
|
return;
|
|
}
|
|
|
|
applyPath(path);
|
|
m_transcoder->start(path, dst);
|
|
return;
|
|
}
|
|
|
|
applyPath(path);
|
|
}
|
|
|
|
void WallpaperService::applyPath(const QString& path) {
|
|
const bool video = isVideoPath(path);
|
|
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
const QString previousPath = m_path;
|
|
m_path = path;
|
|
|
|
if (m_randomTransition) {
|
|
const QString next = randomTransitionFile();
|
|
if (next != m_transitionFile) {
|
|
m_transitionFile = next;
|
|
m_transitionDirty = true;
|
|
}
|
|
}
|
|
|
|
#ifdef ZHAVE_FFMPEG
|
|
const bool videoBackend = m_mpv != nullptr || m_decoder != nullptr;
|
|
#else
|
|
const bool videoBackend = m_mpv != nullptr;
|
|
#endif
|
|
const bool wasVideo = m_isVideo && videoBackend;
|
|
const bool wasImage = !m_image.isNull() && m_texture != 0;
|
|
const bool beginBurst = m_transitions.empty();
|
|
|
|
if (beginBurst) {
|
|
if (wasVideo) {
|
|
m_needCaptureBase = true;
|
|
m_baseIsVideo = true;
|
|
m_baseIsBlack = false;
|
|
m_baseImage = QImage();
|
|
m_baseCrops = m_cache.cropsForPath(previousPath);
|
|
} else if (wasImage) {
|
|
m_needCaptureBase = true;
|
|
m_baseIsVideo = false;
|
|
m_baseIsBlack = false;
|
|
m_baseImage = m_image;
|
|
m_baseCrops = m_cache.cropsForPath(previousPath);
|
|
} else {
|
|
m_needCaptureBase = false;
|
|
m_baseIsVideo = false;
|
|
m_baseIsBlack = m_baseTex == 0;
|
|
m_baseImage = QImage();
|
|
m_baseCrops = m_cache.cropsForPath(previousPath);
|
|
}
|
|
}
|
|
|
|
if (!beginBurst && !m_transitions.empty() &&
|
|
m_transitions.back().isVideo && !m_transitions.back().started &&
|
|
!m_posterReady)
|
|
m_transitions.pop_back();
|
|
|
|
if (!beginBurst && !m_transitions.empty() && m_texture != 0 &&
|
|
!m_transitions.back().isVideo) {
|
|
TransitionLayer& newest = m_transitions.back();
|
|
newest.tex = m_texture;
|
|
newest.image = m_image;
|
|
newest.crops = m_cache.cropsForPath(previousPath);
|
|
newest.ready = true;
|
|
m_texture = 0;
|
|
m_image = QImage();
|
|
}
|
|
|
|
if (!beginBurst && video && !m_transitions.empty() &&
|
|
m_transitions.back().isVideo)
|
|
m_collapseVideo = true;
|
|
|
|
m_crops = m_cache.cropsForPath(path);
|
|
|
|
m_isVideo = video;
|
|
|
|
m_replacementReady = false;
|
|
m_forceDecode = true;
|
|
m_imageChanged = true;
|
|
m_videoReload = true;
|
|
|
|
TransitionLayer layer;
|
|
layer.isVideo = video;
|
|
layer.transitionFile = m_transitionFile;
|
|
m_transitions.push_back(layer);
|
|
|
|
if (static_cast<int>(m_transitions.size()) > kMaxTransitions)
|
|
m_promoteOldest = true;
|
|
|
|
while (static_cast<int>(m_transitions.size()) > kMaxTransitions * 2) {
|
|
if (m_transitions.front().tex != 0)
|
|
m_orphanTextures.push_back(m_transitions.front().tex);
|
|
m_transitions.erase(m_transitions.begin());
|
|
}
|
|
}
|
|
|
|
emit pathChanged();
|
|
|
|
start();
|
|
|
|
queueAll();
|
|
}
|
|
|
|
void WallpaperService::addScreen(const QString& screen) {
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
|
|
if (m_screens.contains(screen)) return;
|
|
|
|
m_screens.append(screen);
|
|
}
|
|
|
|
start();
|
|
|
|
queueAll();
|
|
}
|
|
|
|
void WallpaperService::removeScreen(const QString& screen) {
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
|
|
if (!m_screens.removeOne(screen)) return;
|
|
}
|
|
|
|
queueAll();
|
|
}
|
|
|
|
const QVariantMap& WallpaperService::crops() const {
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
return m_crops;
|
|
}
|
|
|
|
void WallpaperService::setCrops(const QVariantMap& crops) {
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
|
|
if (m_crops == crops) return;
|
|
|
|
m_crops = crops;
|
|
}
|
|
|
|
emit cropsChanged();
|
|
queueAll();
|
|
}
|
|
|
|
void WallpaperService::setCrop(const QString& screen, const QVariantMap& crop) {
|
|
WallpaperSurface* surface = nullptr;
|
|
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
|
|
if (m_crops.value(screen) == crop) return;
|
|
|
|
m_crops.insert(screen, crop);
|
|
|
|
for (auto* candidate : m_surfaces) {
|
|
if (candidate->screen() == screen) {
|
|
surface = candidate;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
emit cropsChanged();
|
|
|
|
if (surface) queue(surface);
|
|
}
|
|
|
|
void WallpaperService::redraw() {
|
|
queueAll();
|
|
}
|
|
|
|
bool WallpaperService::isVideoPath(const QString& path) {
|
|
return kVideoExtensions.contains(QFileInfo(path).suffix().toLower());
|
|
}
|
|
|
|
void WallpaperService::startVideo(const QString& path) {
|
|
if (path.isEmpty()) return;
|
|
|
|
m_videoReady.store(false, std::memory_order_relaxed);
|
|
m_videoLoaded.store(false, std::memory_order_relaxed);
|
|
|
|
#ifdef ZHAVE_FFMPEG
|
|
if (m_useDecoder && !kMpvMode) {
|
|
if (startDecoderVideo(path)) return;
|
|
|
|
qWarning(wpLog) << "WallpaperService: the GPU video player failed, "
|
|
"falling back to mpv";
|
|
m_useDecoder = false;
|
|
m_decoder.reset();
|
|
}
|
|
#endif
|
|
|
|
if (!m_mpv) {
|
|
std::setlocale(LC_NUMERIC, "C");
|
|
|
|
m_mpv = mpv_create();
|
|
if (!m_mpv) {
|
|
qWarning(wpLog) << "WallpaperService: could not create mpv handle";
|
|
return;
|
|
}
|
|
|
|
mpv_set_option_string(m_mpv, "config", "no");
|
|
mpv_set_option_string(m_mpv, "vo", "libmpv");
|
|
mpv_set_option_string(m_mpv, "audio", "no");
|
|
mpv_set_option_string(m_mpv, "loop", "inf");
|
|
mpv_set_option_string(m_mpv, "hwdec", "auto-copy");
|
|
mpv_set_option_string(m_mpv, "video-sync", "display");
|
|
mpv_set_option_string(m_mpv, "framedrop", "decoder");
|
|
mpv_set_option_string(m_mpv, "background-color", "#00000000");
|
|
|
|
if (mpv_initialize(m_mpv) < 0) {
|
|
qWarning(wpLog) << "WallpaperService: mpv_initialize failed";
|
|
mpv_terminate_destroy(m_mpv);
|
|
m_mpv = nullptr;
|
|
return;
|
|
}
|
|
}
|
|
|
|
const QByteArray local = path.toUtf8();
|
|
|
|
initMpvRender();
|
|
|
|
const char* loadCmd[]{"loadfile", local.constData(), nullptr};
|
|
if (mpv_command(m_mpv, loadCmd) < 0)
|
|
qWarning(wpLog) << "WallpaperService: mpv could not load" << path;
|
|
|
|
m_mpvFrameReady.store(true, std::memory_order_relaxed);
|
|
}
|
|
|
|
void WallpaperService::pumpMpvEvents() {
|
|
if (!m_mpv) return;
|
|
|
|
mpv_event* event = nullptr;
|
|
while ((event = mpv_wait_event(m_mpv, 0)) != nullptr &&
|
|
event->event_id != MPV_EVENT_NONE) {
|
|
if (event->event_id == MPV_EVENT_FILE_LOADED) {
|
|
m_videoLoaded.store(true, std::memory_order_relaxed);
|
|
}
|
|
|
|
if (event->event_id == MPV_EVENT_END_FILE) {
|
|
m_videoLoaded.store(false, std::memory_order_relaxed);
|
|
m_videoReady.store(false, std::memory_order_relaxed);
|
|
|
|
auto* data = static_cast<mpv_event_end_file*>(event->data);
|
|
if (data && data->error != MPV_END_FILE_REASON_EOF)
|
|
qWarning(wpLog) << "WallpaperService: mpv ended the file, error"
|
|
<< data->error;
|
|
}
|
|
}
|
|
}
|
|
|
|
void WallpaperService::teardownMpv() {
|
|
if (m_mpvRender) {
|
|
mpv_render_context_free(m_mpvRender);
|
|
m_mpvRender = nullptr;
|
|
}
|
|
|
|
if (m_mpv) {
|
|
mpv_terminate_destroy(m_mpv);
|
|
m_mpv = nullptr;
|
|
}
|
|
|
|
m_mpvFrameReady.store(false, std::memory_order_relaxed);
|
|
m_videoReady.store(false, std::memory_order_relaxed);
|
|
m_videoLoaded.store(false, std::memory_order_relaxed);
|
|
}
|
|
|
|
void* WallpaperService::getMpvProcAddress(void* ctx, const char* name) {
|
|
(void)ctx;
|
|
|
|
if (strcmp(name, "glFenceSync") == 0)
|
|
return reinterpret_cast<void*>(
|
|
+[](GLenum, GLbitfield) -> GLsync { return nullptr; });
|
|
|
|
return reinterpret_cast<void*>(eglGetProcAddress(name));
|
|
}
|
|
|
|
void WallpaperService::mpvUpdate(void* ctx) {
|
|
auto* self = static_cast<WallpaperService*>(ctx);
|
|
|
|
self->m_mpvFrameReady.store(true, std::memory_order_relaxed);
|
|
|
|
if (self->m_videoLoaded.load(std::memory_order_relaxed))
|
|
self->m_videoReady.store(true, std::memory_order_relaxed);
|
|
|
|
self->wake();
|
|
}
|
|
|
|
void WallpaperService::initMpvRender() {
|
|
if (m_mpvRender || !m_mpv) return;
|
|
|
|
mpv_opengl_init_params gl_init{getMpvProcAddress, nullptr};
|
|
|
|
mpv_render_param params[]{
|
|
{MPV_RENDER_PARAM_API_TYPE,
|
|
const_cast<char*>(MPV_RENDER_API_TYPE_OPENGL)},
|
|
{MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init},
|
|
{MPV_RENDER_PARAM_INVALID, nullptr},
|
|
};
|
|
|
|
const int rc = mpv_render_context_create(&m_mpvRender, m_mpv, params);
|
|
if (rc < 0) {
|
|
qWarning(wpLog)
|
|
<< "WallpaperService: could not create mpv render context, code"
|
|
<< rc;
|
|
m_mpvRender = nullptr;
|
|
return;
|
|
}
|
|
|
|
mpv_render_context_set_update_callback(m_mpvRender, mpvUpdate, this);
|
|
}
|
|
|
|
void WallpaperService::renderMpvToSurface(const QSize& size) {
|
|
if (!m_mpvRender) initMpvRender();
|
|
if (!m_mpvRender) return;
|
|
|
|
glViewport(0, 0, size.width(), size.height());
|
|
|
|
mpv_opengl_fbo fbo{0, size.width(), size.height(), GL_RGBA};
|
|
|
|
int flip = 1;
|
|
int block = 0;
|
|
mpv_render_param params[]{
|
|
{MPV_RENDER_PARAM_OPENGL_FBO, &fbo},
|
|
{MPV_RENDER_PARAM_FLIP_Y, &flip},
|
|
{MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME, &block},
|
|
{MPV_RENDER_PARAM_INVALID, nullptr},
|
|
};
|
|
|
|
mpv_render_context_render(m_mpvRender, params);
|
|
}
|
|
|
|
void WallpaperService::createBlackTexture(uint32_t& texture) {
|
|
if (texture != 0) return;
|
|
|
|
const unsigned char pixel[4] = {0, 0, 0, 255};
|
|
|
|
glGenTextures(1, &texture);
|
|
glBindTexture(GL_TEXTURE_2D, texture);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
|
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
glTexImage2D(
|
|
GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
|
|
}
|
|
|
|
void WallpaperService::start() {
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
|
|
if (started || m_path.isEmpty() || m_screens.isEmpty()) return;
|
|
|
|
started = true;
|
|
}
|
|
|
|
auto* interface =
|
|
qApp->nativeInterface<QNativeInterface::QWaylandApplication>();
|
|
if (!interface) {
|
|
qWarning(wpLog) << "WallpaperService: this is not a Wayland session";
|
|
return;
|
|
}
|
|
|
|
m_display = interface->display();
|
|
m_compositor = interface->compositor();
|
|
if (!m_display || !m_compositor) return;
|
|
|
|
m_queue = wl_display_create_queue(m_display);
|
|
if (!m_queue) return;
|
|
|
|
if (pipe2(m_wake, O_CLOEXEC) < 0) {
|
|
qWarning(wpLog)
|
|
<< "WallpaperService: could not make the pipe to wake on";
|
|
return;
|
|
}
|
|
|
|
bindGlobals();
|
|
|
|
if (!setupGl()) return;
|
|
|
|
m_drawing = true;
|
|
m_thread = std::thread([this] { loop(); });
|
|
|
|
wake();
|
|
}
|
|
|
|
void WallpaperService::wake() {
|
|
if (m_wake[1] < 0) return;
|
|
|
|
char byte = 0;
|
|
|
|
if (::write(m_wake[1], &byte, 1) < 0)
|
|
qWarning(wpLog)
|
|
<< "WallpaperService: could not wake the drawing thread";
|
|
}
|
|
|
|
void WallpaperService::queue(WallpaperSurface* surface) {
|
|
surface->markDirty();
|
|
wake();
|
|
}
|
|
|
|
void WallpaperService::queueAll() {
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
|
|
for (auto* surface : m_surfaces)
|
|
surface->markDirty();
|
|
}
|
|
|
|
wake();
|
|
}
|
|
|
|
void WallpaperService::loop() {
|
|
eglBindAPI(kGlesMode ? EGL_OPENGL_ES_API : EGL_OPENGL_API);
|
|
if (m_context == EGL_NO_CONTEXT) {
|
|
if (kGlesMode) {
|
|
const EGLint contextAttributes[] = {
|
|
EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};
|
|
m_context = eglCreateContext(
|
|
m_eglDisplay, m_config, EGL_NO_CONTEXT, contextAttributes);
|
|
} else {
|
|
m_context = eglCreateContext(
|
|
m_eglDisplay, m_config, EGL_NO_CONTEXT, nullptr);
|
|
}
|
|
if (m_context == EGL_NO_CONTEXT) {
|
|
qWarning(wpLog) << "WallpaperService: eglCreateContext failed";
|
|
return;
|
|
}
|
|
}
|
|
|
|
while (!m_stop) {
|
|
present();
|
|
|
|
if (wl_display_prepare_read(m_display) == 0) {
|
|
wl_display_flush(m_display);
|
|
|
|
struct pollfd fds[2] = {
|
|
{wl_display_get_fd(m_display), POLLIN, 0},
|
|
{m_wake[0], POLLIN, 0},
|
|
};
|
|
|
|
int ready = poll(fds, 2, kWaitTimeoutMs);
|
|
|
|
if (ready > 0 && fds[0].revents) {
|
|
wl_display_read_events(m_display);
|
|
continue;
|
|
}
|
|
|
|
if (ready > 0 && fds[1].revents) {
|
|
char byte = 0;
|
|
::read(m_wake[0], &byte, 1);
|
|
}
|
|
|
|
wl_display_cancel_read(m_display);
|
|
continue;
|
|
}
|
|
|
|
struct pollfd fds[1] = {{m_wake[0], POLLIN, 0}};
|
|
|
|
if (poll(fds, 1, kWaitTimeoutMs) <= 0) continue;
|
|
|
|
char byte = 0;
|
|
::read(m_wake[0], &byte, 1);
|
|
}
|
|
}
|
|
|
|
void WallpaperService::present() {
|
|
wl_display_dispatch_queue_pending(m_display, m_queue);
|
|
|
|
addSurfaces();
|
|
|
|
ensureImage();
|
|
|
|
ensurePoster();
|
|
|
|
if (m_isVideo && m_videoReady.load(std::memory_order_relaxed)) {
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
m_replacementReady = true;
|
|
}
|
|
|
|
QList<WallpaperSurface*> surfaces;
|
|
bool videoReload = false;
|
|
QString path;
|
|
bool isVideo = false;
|
|
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
|
|
videoReload = m_videoReload;
|
|
path = m_path;
|
|
isVideo = m_isVideo;
|
|
|
|
const bool animating = !m_transitions.empty();
|
|
const bool newVideoFrame =
|
|
m_isVideo &&
|
|
m_mpvFrameReady.exchange(false, std::memory_order_relaxed);
|
|
|
|
if (animating || newVideoFrame)
|
|
for (auto* surface : m_surfaces)
|
|
surface->markDirty();
|
|
|
|
surfaces = m_surfaces;
|
|
}
|
|
|
|
if (m_context == EGL_NO_CONTEXT) return;
|
|
|
|
if (!m_orphanTextures.empty()) {
|
|
std::lock_guard<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) {
|
|
importTarget = surface->eglSurface();
|
|
break;
|
|
}
|
|
|
|
if (importTarget == EGL_NO_SURFACE) return;
|
|
|
|
if (!eglMakeCurrent(m_eglDisplay, importTarget, importTarget, m_context)) {
|
|
qWarning(wpLog) << "WallpaperService: the context cannot be made "
|
|
"current, eglGetError = 0x"
|
|
<< Qt::hex << unsigned(eglGetError());
|
|
return;
|
|
}
|
|
|
|
if (!m_programsCompiled) {
|
|
m_programsCompiled = true;
|
|
std::lock_guard<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;
|
|
}
|
|
|
|
{
|
|
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) ||
|
|
m_collapseVideo;
|
|
|
|
auto applyVideoBackendSwitch = [&]() {
|
|
if (!videoReload) return;
|
|
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
|
|
if (isVideo) {
|
|
startVideo(path);
|
|
} else if (m_mpv != nullptr) {
|
|
teardownMpv();
|
|
}
|
|
#ifdef ZHAVE_FFMPEG
|
|
else if (m_decoder != nullptr) {
|
|
m_decoder->deleteTexture();
|
|
stopDecoderVideo();
|
|
}
|
|
#endif
|
|
};
|
|
|
|
if (!holdVideoForBase) applyVideoBackendSwitch();
|
|
|
|
#ifdef ZHAVE_FFMPEG
|
|
const bool videoActive =
|
|
(m_mpv != nullptr || m_decoder != nullptr) &&
|
|
(m_isVideo || m_baseIsVideo || m_collapseVideo || m_promoteOldest);
|
|
#else
|
|
const bool videoActive =
|
|
m_mpv != nullptr &&
|
|
(m_isVideo || m_baseIsVideo || m_collapseVideo || m_promoteOldest);
|
|
#endif
|
|
|
|
if (videoActive && m_mpv != nullptr) pumpMpvEvents();
|
|
|
|
#ifdef ZHAVE_FFMPEG
|
|
if (videoActive && m_decoder != nullptr) m_decoder->importCurrentFrame();
|
|
#endif
|
|
|
|
for (auto* surface : surfaces)
|
|
drawSurface(surface);
|
|
|
|
if (holdVideoForBase) applyVideoBackendSwitch();
|
|
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
|
|
if (m_collapseVideo) {
|
|
for (size_t i = 0; i + 1 < m_transitions.size(); ++i)
|
|
if (m_transitions[i].tex != 0)
|
|
m_orphanTextures.push_back(m_transitions[i].tex);
|
|
if (m_transitions.size() > 1)
|
|
m_transitions.erase(
|
|
m_transitions.begin(), m_transitions.end() - 1);
|
|
m_collapseVideo = false;
|
|
}
|
|
|
|
if (m_promoteOldest && !m_transitions.empty()) {
|
|
if (m_transitions.front().tex != 0)
|
|
m_orphanTextures.push_back(m_transitions.front().tex);
|
|
m_transitions.erase(m_transitions.begin());
|
|
m_promoteOldest = static_cast<int>(m_transitions.size()) >
|
|
kMaxTransitions;
|
|
}
|
|
|
|
if (m_newestDone) {
|
|
for (const auto& layer : m_transitions)
|
|
if (layer.tex != 0) m_orphanTextures.push_back(layer.tex);
|
|
m_transitions.clear();
|
|
m_newestDone = false;
|
|
m_baseIsBlack = false;
|
|
m_baseImage = QImage();
|
|
m_baseCrops = QVariantMap();
|
|
}
|
|
|
|
if (m_needCaptureBase) {
|
|
m_needCaptureBase = false;
|
|
m_baseIsVideo = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void WallpaperService::addSurfaces() {
|
|
if (!started || !m_layerShell || !m_viewporter || !m_scaleManager) return;
|
|
|
|
QList<WallpaperSurface*> gone;
|
|
QList<QString> needed;
|
|
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
|
|
QList<WallpaperSurface*> kept;
|
|
for (auto* surface : m_surfaces) {
|
|
if (m_screens.contains(surface->screen()))
|
|
kept.append(surface);
|
|
else
|
|
gone.append(surface);
|
|
}
|
|
|
|
m_surfaces = kept;
|
|
|
|
for (const auto& screen : m_screens) {
|
|
bool exists = false;
|
|
for (auto* surface : m_surfaces) {
|
|
if (surface->screen() == screen) {
|
|
exists = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!exists) needed.append(screen);
|
|
}
|
|
}
|
|
|
|
for (auto* surface : gone)
|
|
delete surface;
|
|
|
|
for (const auto& screen : needed)
|
|
addSurface(screen);
|
|
}
|
|
|
|
void WallpaperService::registryGlobal(
|
|
void* data,
|
|
struct wl_registry* registry,
|
|
uint32_t name,
|
|
const char* interface,
|
|
uint32_t version) {
|
|
auto* service = static_cast<WallpaperService*>(data);
|
|
|
|
void* proxy = nullptr;
|
|
|
|
if (strcmp(interface, "zwlr_layer_shell_v1") == 0) {
|
|
proxy = new QtWayland::zwlr_layer_shell_v1(
|
|
registry, int(name), int(version));
|
|
} else if (strcmp(interface, "wp_viewporter") == 0) {
|
|
proxy = new QtWayland::wp_viewporter(registry, int(name), int(version));
|
|
} else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0) {
|
|
proxy = new QtWayland::wp_fractional_scale_manager_v1(
|
|
registry, int(name), int(version));
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
service->storeGlobal(interface, proxy);
|
|
|
|
service->queueAll();
|
|
}
|
|
|
|
void WallpaperService::storeGlobal(const char* interface, void* proxy) {
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
|
|
if (strcmp(interface, "zwlr_layer_shell_v1") == 0)
|
|
m_layerShell = static_cast<QtWayland::zwlr_layer_shell_v1*>(proxy);
|
|
else if (strcmp(interface, "wp_viewporter") == 0)
|
|
m_viewporter = static_cast<QtWayland::wp_viewporter*>(proxy);
|
|
else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0)
|
|
m_scaleManager =
|
|
static_cast<QtWayland::wp_fractional_scale_manager_v1*>(proxy);
|
|
}
|
|
|
|
void WallpaperService::bindGlobals() {
|
|
m_registry = wl_display_get_registry(m_display);
|
|
|
|
static const struct wl_registry_listener listener = {
|
|
registryGlobal,
|
|
nullptr,
|
|
};
|
|
|
|
wl_registry_add_listener(m_registry, &listener, this);
|
|
}
|
|
|
|
bool WallpaperService::setupGl() {
|
|
const char* extensions = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
|
|
if (extensions && strstr(extensions, "EGL_EXT_platform_wayland")) {
|
|
m_eglDisplay =
|
|
eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_EXT, m_display, nullptr);
|
|
} else {
|
|
m_eglDisplay =
|
|
eglGetDisplay(reinterpret_cast<EGLNativeDisplayType>(m_display));
|
|
}
|
|
|
|
if (m_eglDisplay == EGL_NO_DISPLAY) {
|
|
qWarning(wpLog) << "WallpaperService: no EGL display";
|
|
return false;
|
|
}
|
|
|
|
EGLint major = 0, minor = 0;
|
|
if (!eglInitialize(m_eglDisplay, &major, &minor)) {
|
|
qWarning(wpLog) << "WallpaperService: eglInitialize failed";
|
|
return false;
|
|
}
|
|
|
|
const EGLint configAttributes[] = {
|
|
EGL_SURFACE_TYPE,
|
|
EGL_WINDOW_BIT,
|
|
EGL_RENDERABLE_TYPE,
|
|
kGlesMode ? EGL_OPENGL_ES2_BIT : (EGL_OPENGL_BIT | EGL_OPENGL_ES2_BIT),
|
|
EGL_RED_SIZE,
|
|
8,
|
|
EGL_GREEN_SIZE,
|
|
8,
|
|
EGL_BLUE_SIZE,
|
|
8,
|
|
EGL_ALPHA_SIZE,
|
|
8,
|
|
EGL_NONE};
|
|
|
|
EGLint count = 0;
|
|
if (!eglChooseConfig(m_eglDisplay, configAttributes, &m_config, 1, &count) ||
|
|
count == 0) {
|
|
qWarning(wpLog)
|
|
<< "WallpaperService: no EGL config for window surfaces";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
uint32_t WallpaperService::createProgram() {
|
|
const QByteArray vsSrc = shaderSource("wallpaper.vert", false);
|
|
const QByteArray fsSrc = shaderSource("wallpaper.frag", true);
|
|
const char* vs = vsSrc.constData();
|
|
const char* fs = fsSrc.constData();
|
|
|
|
uint32_t vertex = glCreateShader(GL_VERTEX_SHADER);
|
|
glShaderSource(vertex, 1, &vs, nullptr);
|
|
glCompileShader(vertex);
|
|
logShader(vertex, false);
|
|
|
|
uint32_t fragment = glCreateShader(GL_FRAGMENT_SHADER);
|
|
glShaderSource(fragment, 1, &fs, nullptr);
|
|
glCompileShader(fragment);
|
|
logShader(fragment, false);
|
|
|
|
uint32_t program = glCreateProgram();
|
|
glAttachShader(program, vertex);
|
|
glAttachShader(program, fragment);
|
|
glLinkProgram(program);
|
|
logShader(program, true);
|
|
|
|
if (glGetUniformLocation(program, "size") < 0 ||
|
|
glGetUniformLocation(program, "crop") < 0 ||
|
|
glGetUniformLocation(program, "alpha") < 0 ||
|
|
glGetUniformLocation(program, "source") < 0) {
|
|
qWarning(wpLog) << "WallpaperService: the program does not have the "
|
|
"uniforms it needs";
|
|
glDeleteProgram(program);
|
|
return 0;
|
|
}
|
|
|
|
m_sizeUniform = glGetUniformLocation(program, "size");
|
|
m_cropUniform = glGetUniformLocation(program, "crop");
|
|
m_alphaUniform = glGetUniformLocation(program, "alpha");
|
|
m_sourceUniform = glGetUniformLocation(program, "source");
|
|
|
|
glUseProgram(program);
|
|
glUniform1i(GLint(m_sourceUniform), 0);
|
|
|
|
glDeleteShader(vertex);
|
|
glDeleteShader(fragment);
|
|
return program;
|
|
}
|
|
|
|
WallpaperService::TransProgram WallpaperService::createTransitionProgram(
|
|
const QString& file) {
|
|
TransProgram p;
|
|
|
|
const QByteArray vsSrc = shaderSource("wallpaper.vert", false);
|
|
const QByteArray fsSrc = shaderSource(file, true);
|
|
const char* vs = vsSrc.constData();
|
|
const char* fs = fsSrc.constData();
|
|
|
|
uint32_t vertex = glCreateShader(GL_VERTEX_SHADER);
|
|
glShaderSource(vertex, 1, &vs, nullptr);
|
|
glCompileShader(vertex);
|
|
logShader(vertex, false);
|
|
|
|
uint32_t fragment = glCreateShader(GL_FRAGMENT_SHADER);
|
|
glShaderSource(fragment, 1, &fs, nullptr);
|
|
glCompileShader(fragment);
|
|
logShader(fragment, false);
|
|
|
|
uint32_t program = glCreateProgram();
|
|
glAttachShader(program, vertex);
|
|
glAttachShader(program, fragment);
|
|
glLinkProgram(program);
|
|
logShader(program, true);
|
|
|
|
if (glGetUniformLocation(program, "size") < 0 ||
|
|
glGetUniformLocation(program, "from") < 0 ||
|
|
glGetUniformLocation(program, "to") < 0 ||
|
|
glGetUniformLocation(program, "fromCrop") < 0 ||
|
|
glGetUniformLocation(program, "toCrop") < 0 ||
|
|
glGetUniformLocation(program, "fromFlip") < 0 ||
|
|
glGetUniformLocation(program, "toFlip") < 0 ||
|
|
glGetUniformLocation(program, "progress") < 0) {
|
|
qWarning(wpLog) << "WallpaperService: the transition program does not "
|
|
"have the uniforms it needs";
|
|
glDeleteProgram(program);
|
|
glDeleteShader(vertex);
|
|
glDeleteShader(fragment);
|
|
return p;
|
|
}
|
|
|
|
p.id = program;
|
|
p.size = glGetUniformLocation(program, "size");
|
|
p.progress = glGetUniformLocation(program, "progress");
|
|
p.from = glGetUniformLocation(program, "from");
|
|
p.to = glGetUniformLocation(program, "to");
|
|
p.fromCrop = glGetUniformLocation(program, "fromCrop");
|
|
p.toCrop = glGetUniformLocation(program, "toCrop");
|
|
p.fromFlip = glGetUniformLocation(program, "fromFlip");
|
|
p.toFlip = glGetUniformLocation(program, "toFlip");
|
|
|
|
glUseProgram(program);
|
|
glUniform1i(GLint(p.from), 0);
|
|
glUniform1i(GLint(p.to), 1);
|
|
|
|
glDeleteShader(vertex);
|
|
glDeleteShader(fragment);
|
|
return p;
|
|
}
|
|
|
|
WallpaperService::TransProgram WallpaperService::programFor(
|
|
const QString& file) {
|
|
auto cached = m_transPrograms.constFind(file);
|
|
if (cached != m_transPrograms.constEnd()) return cached.value();
|
|
|
|
TransProgram p = createTransitionProgram(file);
|
|
if (p.id != 0) m_transPrograms.insert(file, p);
|
|
return p;
|
|
}
|
|
|
|
bool WallpaperService::ensureTransitionProgram() {
|
|
if (m_neutralProgram.id == 0)
|
|
m_neutralProgram =
|
|
programFor(QStringLiteral("wallpaper_trans_fade.frag"));
|
|
|
|
return m_neutralProgram.id != 0;
|
|
}
|
|
|
|
QString WallpaperService::transitionFileFor(
|
|
ZShell::config::WallpaperTransition::Enum value) {
|
|
if (value == ZShell::config::WallpaperTransition::Random)
|
|
return randomTransitionFile();
|
|
return QString(transitionFile(value));
|
|
}
|
|
|
|
QString WallpaperService::randomTransitionFile() {
|
|
const QStringList& files = transitionFilesList();
|
|
if (files.isEmpty()) return QStringLiteral("wallpaper_trans_fade.frag");
|
|
if (files.size() == 1) return files.at(0);
|
|
|
|
if (m_transitionUses.size() != files.size())
|
|
m_transitionUses.fill(0, files.size());
|
|
|
|
double total = 0.0;
|
|
QVector<double> weights(files.size(), 0.0);
|
|
for (int i = 0; i < files.size(); ++i) {
|
|
if (files.at(i) == m_lastTransition) continue;
|
|
weights[i] = 1.0 / double(m_transitionUses.at(i) + 1);
|
|
total += weights[i];
|
|
}
|
|
|
|
if (total <= 0.0) return files.at(0);
|
|
|
|
double pick = QRandomGenerator::global()->generateDouble() * total;
|
|
int chosen = -1;
|
|
for (int i = 0; i < files.size(); ++i) {
|
|
if (weights[i] <= 0.0) continue;
|
|
pick -= weights[i];
|
|
if (pick <= 0.0) {
|
|
chosen = i;
|
|
break;
|
|
}
|
|
}
|
|
if (chosen < 0) chosen = int(files.size()) - 1;
|
|
|
|
++m_transitionUses[chosen];
|
|
m_lastTransition = files.at(chosen);
|
|
return files.at(chosen);
|
|
}
|
|
|
|
void WallpaperService::ensureCapture(const QSize& size) {
|
|
if (m_captureFbo == 0) {
|
|
glGenFramebuffers(1, &m_captureFbo);
|
|
glGenTextures(1, &m_captureTex);
|
|
|
|
glBindTexture(GL_TEXTURE_2D, m_captureTex);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
}
|
|
|
|
if (m_captureSize != size) {
|
|
glBindTexture(GL_TEXTURE_2D, m_captureTex);
|
|
glTexImage2D(
|
|
GL_TEXTURE_2D,
|
|
0,
|
|
GL_RGBA8,
|
|
size.width(),
|
|
size.height(),
|
|
0,
|
|
GL_RGBA,
|
|
GL_UNSIGNED_BYTE,
|
|
nullptr);
|
|
m_captureSize = size;
|
|
}
|
|
|
|
glBindFramebuffer(GL_FRAMEBUFFER, m_captureFbo);
|
|
glFramebufferTexture2D(
|
|
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_captureTex, 0);
|
|
|
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
|
qWarning(wpLog) << "WallpaperService: the capture framebuffer is "
|
|
"incomplete";
|
|
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
}
|
|
|
|
#ifdef ZHAVE_FFMPEG
|
|
uint32_t WallpaperService::createVideoProgram() {
|
|
const QByteArray vsSrc = shaderSource("wallpaper.vert", false);
|
|
const QByteArray fsSrc = shaderSource("wallpaper_video.frag", true);
|
|
const char* vs = vsSrc.constData();
|
|
const char* fs = fsSrc.constData();
|
|
|
|
uint32_t vertex = glCreateShader(GL_VERTEX_SHADER);
|
|
glShaderSource(vertex, 1, &vs, nullptr);
|
|
glCompileShader(vertex);
|
|
logShader(vertex, false);
|
|
|
|
uint32_t fragment = glCreateShader(GL_FRAGMENT_SHADER);
|
|
glShaderSource(fragment, 1, &fs, nullptr);
|
|
glCompileShader(fragment);
|
|
logShader(fragment, false);
|
|
|
|
uint32_t program = glCreateProgram();
|
|
glAttachShader(program, vertex);
|
|
glAttachShader(program, fragment);
|
|
glLinkProgram(program);
|
|
logShader(program, true);
|
|
|
|
if (glGetUniformLocation(program, "size") < 0 ||
|
|
glGetUniformLocation(program, "srcSize") < 0 ||
|
|
glGetUniformLocation(program, "crop") < 0 ||
|
|
glGetUniformLocation(program, "alpha") < 0 ||
|
|
glGetUniformLocation(program, "luma") < 0 ||
|
|
glGetUniformLocation(program, "chroma") < 0 ||
|
|
glGetUniformLocation(program, "matrix") < 0 ||
|
|
glGetUniformLocation(program, "range") < 0 ||
|
|
glGetUniformLocation(program, "chromaOff") < 0) {
|
|
qWarning(wpLog)
|
|
<< "WallpaperService: the video program does not have the "
|
|
"uniforms it needs";
|
|
glDeleteProgram(program);
|
|
return 0;
|
|
}
|
|
|
|
m_videoSizeUniform = glGetUniformLocation(program, "size");
|
|
m_videoSrcSizeUniform = glGetUniformLocation(program, "srcSize");
|
|
m_videoCropUniform = glGetUniformLocation(program, "crop");
|
|
m_videoAlphaUniform = glGetUniformLocation(program, "alpha");
|
|
m_videoLumaUniform = glGetUniformLocation(program, "luma");
|
|
m_videoChromaUniform = glGetUniformLocation(program, "chroma");
|
|
m_videoMatrixUniform = glGetUniformLocation(program, "matrix");
|
|
m_videoRangeUniform = glGetUniformLocation(program, "range");
|
|
m_videoChromaOffUniform = glGetUniformLocation(program, "chromaOff");
|
|
|
|
glDeleteShader(vertex);
|
|
glDeleteShader(fragment);
|
|
return program;
|
|
}
|
|
|
|
void WallpaperService::drawVideoTexture(
|
|
uint32_t lumaTex,
|
|
uint32_t chromaTex,
|
|
int matrix,
|
|
int range,
|
|
const QRectF& rect,
|
|
float sizeW,
|
|
float sizeH,
|
|
float srcW,
|
|
float srcH,
|
|
float alpha) {
|
|
glActiveTexture(GL_TEXTURE0);
|
|
glBindTexture(GL_TEXTURE_2D, lumaTex);
|
|
glUniform1i(GLint(m_videoLumaUniform), 0);
|
|
|
|
glActiveTexture(GL_TEXTURE1);
|
|
glBindTexture(GL_TEXTURE_2D, chromaTex);
|
|
glUniform1i(GLint(m_videoChromaUniform), 1);
|
|
|
|
glUniform1i(GLint(m_videoMatrixUniform), matrix);
|
|
glUniform1i(GLint(m_videoRangeUniform), range);
|
|
|
|
glUniform2f(
|
|
GLint(m_videoChromaOffUniform),
|
|
range == 0 && srcW > 0 ? 0.5f / srcW : 0.0f,
|
|
0.0f);
|
|
|
|
glUniform2f(GLint(m_videoSizeUniform), sizeW, sizeH);
|
|
glUniform2f(GLint(m_videoSrcSizeUniform), srcW, srcH);
|
|
glUniform4f(
|
|
GLint(m_videoCropUniform),
|
|
GLfloat(rect.x()),
|
|
GLfloat(rect.y()),
|
|
GLfloat(rect.width()),
|
|
GLfloat(rect.height()));
|
|
glUniform1f(GLint(m_videoAlphaUniform), alpha);
|
|
|
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
}
|
|
|
|
bool WallpaperService::startDecoderVideo(const QString& path) {
|
|
if (!m_decoder)
|
|
m_decoder = createVideoDecoder(m_eglDisplay, backendFromConfig());
|
|
if (!m_decoder) return false;
|
|
|
|
m_decoder->setReadyCallback([this]() {
|
|
m_videoLoaded.store(true, std::memory_order_relaxed);
|
|
m_videoReady.store(true, std::memory_order_relaxed);
|
|
queueAll();
|
|
});
|
|
|
|
m_decoder->setFrameCallback([this]() {
|
|
m_videoReady.store(true, std::memory_order_relaxed);
|
|
queueAll();
|
|
});
|
|
|
|
if (!m_decoder->start(path)) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
void WallpaperService::stopDecoderVideo() {
|
|
if (!m_decoder) return;
|
|
|
|
m_decoder->stop();
|
|
m_decoder.reset();
|
|
|
|
m_videoReady.store(false, std::memory_order_relaxed);
|
|
m_videoLoaded.store(false, std::memory_order_relaxed);
|
|
}
|
|
#endif
|
|
|
|
uint32_t WallpaperService::renderVideoIntoTexture(const QSize& size) {
|
|
ensureCapture(size);
|
|
if (m_captureFbo == 0 || m_captureTex == 0) return 0;
|
|
|
|
glBindFramebuffer(GL_FRAMEBUFFER, m_captureFbo);
|
|
glViewport(0, 0, size.width(), size.height());
|
|
glDisable(GL_BLEND);
|
|
|
|
#ifdef ZHAVE_FFMPEG
|
|
if (m_decoder) {
|
|
if (m_decoder->textureId() != 0) {
|
|
if (m_videoProgram == 0) m_videoProgram = createVideoProgram();
|
|
|
|
if (m_videoProgram != 0) {
|
|
glUseProgram(m_videoProgram);
|
|
const QSize src = m_decoder->frameSize();
|
|
drawVideoTexture(
|
|
m_decoder->textureId(),
|
|
m_decoder->chromaTextureId(),
|
|
m_decoder->colorMatrix(),
|
|
m_decoder->colorRange(),
|
|
QRectF(0.0, 0.0, 1.0, 1.0),
|
|
float(size.width()),
|
|
float(size.height()),
|
|
src.width() > 0 ? float(src.width()) : float(size.width()),
|
|
src.height() > 0 ? float(src.height())
|
|
: float(size.height()),
|
|
1.0f);
|
|
}
|
|
}
|
|
} else
|
|
#endif
|
|
{
|
|
if (!m_mpvRender) initMpvRender();
|
|
if (!m_mpvRender) {
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
return m_captureTex;
|
|
}
|
|
|
|
glViewport(0, 0, size.width(), size.height());
|
|
|
|
mpv_opengl_fbo fbo{
|
|
int(m_captureFbo), size.width(), size.height(), GL_RGBA};
|
|
|
|
int flip = 1;
|
|
int block = 0;
|
|
mpv_render_param params[]{
|
|
{MPV_RENDER_PARAM_OPENGL_FBO, &fbo},
|
|
{MPV_RENDER_PARAM_FLIP_Y, &flip},
|
|
{MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME, &block},
|
|
{MPV_RENDER_PARAM_INVALID, nullptr},
|
|
};
|
|
|
|
mpv_render_context_render(m_mpvRender, params);
|
|
}
|
|
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
return m_captureTex;
|
|
}
|
|
|
|
|
|
void WallpaperService::uploadTexture(uint32_t& texture, const QImage& image) {
|
|
if (image.isNull()) return;
|
|
|
|
if (texture == 0) {
|
|
glGenTextures(1, &texture);
|
|
glBindTexture(GL_TEXTURE_2D, texture);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
} else {
|
|
glBindTexture(GL_TEXTURE_2D, texture);
|
|
}
|
|
|
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
glTexImage2D(
|
|
GL_TEXTURE_2D,
|
|
0,
|
|
GL_RGBA8,
|
|
image.width(),
|
|
image.height(),
|
|
0,
|
|
GL_RGBA,
|
|
GL_UNSIGNED_BYTE,
|
|
image.constBits());
|
|
}
|
|
|
|
void WallpaperService::uploadImage() {
|
|
uploadTexture(m_texture, m_image);
|
|
}
|
|
|
|
void WallpaperService::drawTexture(
|
|
uint32_t texture,
|
|
const QRectF& rect,
|
|
float sizeW,
|
|
float sizeH,
|
|
float alpha) {
|
|
glActiveTexture(GL_TEXTURE0);
|
|
glBindTexture(GL_TEXTURE_2D, texture);
|
|
glUniform1i(GLint(m_sourceUniform), 0);
|
|
glUniform2f(GLint(m_sizeUniform), sizeW, sizeH);
|
|
glUniform4f(
|
|
GLint(m_cropUniform),
|
|
GLfloat(rect.x()),
|
|
GLfloat(rect.y()),
|
|
GLfloat(rect.width()),
|
|
GLfloat(rect.height()));
|
|
glUniform1f(GLint(m_alphaUniform), alpha);
|
|
|
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
}
|
|
|
|
void WallpaperService::ensureImage() {
|
|
QSize largest;
|
|
QString path;
|
|
bool needDecode = false;
|
|
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
|
|
if (m_isVideo) return;
|
|
|
|
largest = largestBufferSize();
|
|
if (largest.isEmpty()) return;
|
|
|
|
path = m_path;
|
|
|
|
if (m_stagedReady && m_stagedPath == path) {
|
|
m_image = m_stagedImage;
|
|
m_stagedImage = QImage();
|
|
m_stagedReady = false;
|
|
m_decodeSize = largest;
|
|
m_imageChanged = true;
|
|
m_replacementReady = true;
|
|
} else {
|
|
m_stagedReady = false;
|
|
m_stagedImage = QImage();
|
|
|
|
const bool grew = m_decodeSize.isEmpty() ||
|
|
largest.width() > m_decodeSize.width() ||
|
|
largest.height() > m_decodeSize.height();
|
|
|
|
if (!m_decoding && (m_forceDecode || path != m_decodePath ||
|
|
m_image.isNull() || grew)) {
|
|
m_forceDecode = false;
|
|
m_decoding = true;
|
|
m_decodePath = path;
|
|
needDecode = true;
|
|
} else {
|
|
m_forceDecode = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!needDecode) return;
|
|
|
|
QThreadPool::globalInstance()->start([this, path, largest]() {
|
|
const QImage image = m_cache.decodeImage(path, largest);
|
|
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
m_stagedImage = image;
|
|
m_stagedPath = path;
|
|
m_stagedReady = !image.isNull();
|
|
m_decoding = false;
|
|
});
|
|
|
|
wake();
|
|
}
|
|
|
|
void WallpaperService::ensurePoster() {
|
|
QString path;
|
|
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
|
|
if (!m_isVideo) return;
|
|
|
|
path = m_path;
|
|
|
|
if (m_posterPath == path && (m_posterReady || m_posterLoading)) return;
|
|
if (m_posterLoading) return;
|
|
|
|
m_posterPath = path;
|
|
m_posterReady = false;
|
|
m_posterLoading = true;
|
|
m_posterImage = QImage();
|
|
}
|
|
|
|
QThreadPool::globalInstance()->start([this, path]() {
|
|
const QByteArray blob = m_cache.posterBlob(path);
|
|
|
|
QImage image;
|
|
if (!blob.isEmpty()) {
|
|
image = QImage::fromData(blob, "JPEG")
|
|
.convertToFormat(QImage::Format_RGBA8888);
|
|
}
|
|
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
m_posterImage = image;
|
|
m_posterReady = !image.isNull();
|
|
m_posterChanged = m_posterReady;
|
|
m_posterLoading = false;
|
|
|
|
if (!m_posterReady)
|
|
qWarning(wpLog)
|
|
<< "WallpaperService: poster decode failed for" << path;
|
|
});
|
|
|
|
wake();
|
|
}
|
|
|
|
void WallpaperService::invalidateCache() {
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
if (m_isVideo) return;
|
|
m_forceDecode = true;
|
|
}
|
|
|
|
wake();
|
|
}
|
|
|
|
QSize WallpaperService::largestBufferSize() const {
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
|
|
QSize largest;
|
|
|
|
for (auto* surface : m_surfaces) {
|
|
QSize buffer = surface->bufferSize();
|
|
if (buffer.isEmpty()) continue;
|
|
|
|
if (largest.isEmpty() || qint64(buffer.width()) * buffer.height() >
|
|
qint64(largest.width()) * largest.height())
|
|
largest = buffer;
|
|
}
|
|
|
|
return largest;
|
|
}
|
|
|
|
void WallpaperService::notifyBufferSizes() {
|
|
QMetaObject::invokeMethod(
|
|
this, "largestBufferSizeChanged", Qt::QueuedConnection);
|
|
}
|
|
|
|
QRectF WallpaperService::sourceRect(
|
|
const QVariantMap& crops,
|
|
const QString& screen,
|
|
const QSize& size,
|
|
const QSize& image) const {
|
|
QVariantMap crop = cropFor(crops, screen);
|
|
|
|
QRectF rect(
|
|
crop.value("x", 0.0).toDouble(),
|
|
crop.value("y", 0.0).toDouble(),
|
|
crop.value("width", 1.0).toDouble(),
|
|
crop.value("height", 1.0).toDouble());
|
|
|
|
if (size.isEmpty() || rect.isEmpty() || image.isEmpty()) return rect;
|
|
|
|
qreal imageWidth = qreal(image.width());
|
|
qreal imageHeight = qreal(image.height());
|
|
|
|
QRectF pixels(
|
|
rect.x() * imageWidth,
|
|
rect.y() * imageHeight,
|
|
rect.width() * imageWidth,
|
|
rect.height() * imageHeight);
|
|
|
|
qreal targetRatio = qreal(size.width()) / qreal(size.height());
|
|
qreal rectRatio = pixels.width() / pixels.height();
|
|
|
|
if (rectRatio > targetRatio) {
|
|
qreal newWidth = pixels.height() * targetRatio;
|
|
pixels.setX(pixels.x() + (pixels.width() - newWidth) / 2.0);
|
|
pixels.setWidth(newWidth);
|
|
} else if (rectRatio < targetRatio) {
|
|
qreal newHeight = pixels.width() / targetRatio;
|
|
pixels.setY(pixels.y() + (pixels.height() - newHeight) / 2.0);
|
|
pixels.setHeight(newHeight);
|
|
}
|
|
|
|
return QRectF(
|
|
pixels.x() / imageWidth,
|
|
pixels.y() / imageHeight,
|
|
pixels.width() / imageWidth,
|
|
pixels.height() / imageHeight);
|
|
}
|
|
|
|
void WallpaperService::drawSurface(WallpaperSurface* surface) {
|
|
if (!m_transitions.empty()) {
|
|
drawSurfaceTransition(surface);
|
|
return;
|
|
}
|
|
|
|
if (!surface->needsDraw() || !surface->frameReady()) return;
|
|
|
|
EGLSurface target = surface->eglSurface();
|
|
if (m_context == EGL_NO_CONTEXT || target == EGL_NO_SURFACE ||
|
|
surface->bufferSize().isEmpty())
|
|
return;
|
|
|
|
if (!eglMakeCurrent(m_eglDisplay, target, target, m_context)) {
|
|
qWarning(wpLog) << "WallpaperService: the context cannot be made "
|
|
"current, eglGetError = 0x"
|
|
<< Qt::hex << unsigned(eglGetError());
|
|
return;
|
|
}
|
|
|
|
eglSwapInterval(m_eglDisplay, 0);
|
|
|
|
if (m_vao == 0) {
|
|
glGenVertexArrays(1, &m_vao);
|
|
glBindVertexArray(m_vao);
|
|
}
|
|
|
|
struct ContextRelease {
|
|
EGLDisplay display;
|
|
~ContextRelease() {
|
|
eglMakeCurrent(
|
|
display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
|
}
|
|
} contextRelease{m_eglDisplay};
|
|
|
|
QVariantMap crops;
|
|
QSize size;
|
|
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
|
|
crops = m_crops;
|
|
size = surface->bufferSize();
|
|
|
|
if (size.isEmpty()) return;
|
|
|
|
if (m_program == 0) m_program = createProgram();
|
|
if (m_program == 0) return;
|
|
|
|
const bool videoNow = m_isVideo;
|
|
|
|
if (videoNow && !m_videoReady.load(std::memory_order_relaxed)) return;
|
|
|
|
#ifdef ZHAVE_FFMPEG
|
|
const bool videoAsBackground =
|
|
videoNow && (m_mpv != nullptr || m_decoder != nullptr);
|
|
#else
|
|
const bool videoAsBackground = videoNow && m_mpv != nullptr;
|
|
#endif
|
|
|
|
if (videoAsBackground) {
|
|
glDisable(GL_BLEND);
|
|
glViewport(0, 0, size.width(), size.height());
|
|
|
|
#ifdef ZHAVE_FFMPEG
|
|
if (m_decoder) {
|
|
if (m_decoder->textureId() != 0) {
|
|
if (m_videoProgram == 0)
|
|
m_videoProgram = createVideoProgram();
|
|
|
|
if (m_videoProgram != 0) {
|
|
glUseProgram(m_videoProgram);
|
|
const QSize src = m_decoder->frameSize();
|
|
drawVideoTexture(
|
|
m_decoder->textureId(),
|
|
m_decoder->chromaTextureId(),
|
|
m_decoder->colorMatrix(),
|
|
m_decoder->colorRange(),
|
|
QRectF(0.0, 0.0, 1.0, 1.0),
|
|
float(size.width()),
|
|
float(size.height()),
|
|
src.width() > 0 ? float(src.width())
|
|
: float(size.width()),
|
|
src.height() > 0 ? float(src.height())
|
|
: float(size.height()),
|
|
1.0f);
|
|
}
|
|
}
|
|
} else
|
|
#endif
|
|
{
|
|
if (!m_mpvRender) initMpvRender();
|
|
if (!m_mpvRender) return;
|
|
|
|
renderMpvToSurface(size);
|
|
}
|
|
} else {
|
|
glUseProgram(m_program);
|
|
glViewport(0, 0, size.width(), size.height());
|
|
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
}
|
|
|
|
if (!videoNow) {
|
|
const bool upload = m_imageChanged;
|
|
m_imageChanged = false;
|
|
|
|
if (m_texture == 0 || upload) uploadImage();
|
|
|
|
if (m_texture == 0 || m_image.isNull()) return;
|
|
}
|
|
|
|
glUseProgram(m_program);
|
|
glEnable(GL_BLEND);
|
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
|
|
const float w = float(size.width());
|
|
const float h = float(size.height());
|
|
|
|
if (!videoNow) {
|
|
const QRectF rect =
|
|
sourceRect(crops, surface->screen(), size, m_image.size());
|
|
drawTexture(m_texture, rect, w, h, 1.0f);
|
|
}
|
|
}
|
|
|
|
surface->requestFrame();
|
|
|
|
if (!eglSwapBuffers(m_eglDisplay, target)) {
|
|
qWarning(wpLog)
|
|
<< "WallpaperService: eglSwapBuffers failed, eglGetError = 0x"
|
|
<< QString::number(unsigned(eglGetError()), 16);
|
|
return;
|
|
}
|
|
|
|
if (m_isVideo && m_mpvRender) mpv_render_context_report_swap(m_mpvRender);
|
|
|
|
surface->endDraw();
|
|
}
|
|
|
|
void WallpaperService::ensureBaseFbo() {
|
|
if (m_baseFbo != 0) return;
|
|
|
|
glGenFramebuffers(1, &m_baseFbo);
|
|
glGenTextures(1, &m_baseTex);
|
|
|
|
glBindTexture(GL_TEXTURE_2D, m_baseTex);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
}
|
|
|
|
void WallpaperService::ensureAccum(const QSize& size) {
|
|
if (m_accumFbo == 0) {
|
|
glGenFramebuffers(1, &m_accumFbo);
|
|
glGenTextures(1, &m_accumA);
|
|
glGenTextures(1, &m_accumB);
|
|
|
|
for (uint32_t tex : {m_accumA, m_accumB}) {
|
|
glBindTexture(GL_TEXTURE_2D, tex);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
}
|
|
}
|
|
|
|
if (m_accumSize != size) {
|
|
for (uint32_t tex : {m_accumA, m_accumB}) {
|
|
glBindTexture(GL_TEXTURE_2D, tex);
|
|
glTexImage2D(
|
|
GL_TEXTURE_2D,
|
|
0,
|
|
GL_RGBA8,
|
|
size.width(),
|
|
size.height(),
|
|
0,
|
|
GL_RGBA,
|
|
GL_UNSIGNED_BYTE,
|
|
nullptr);
|
|
}
|
|
m_accumSize = size;
|
|
}
|
|
}
|
|
|
|
void WallpaperService::bindRtt(uint32_t fbo, uint32_t tex, const QSize& size) {
|
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
glFramebufferTexture2D(
|
|
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
|
|
glViewport(0, 0, size.width(), size.height());
|
|
}
|
|
|
|
void WallpaperService::drawTransitionPair(
|
|
const TransProgram& program,
|
|
uint32_t fromTex,
|
|
float fromFlip,
|
|
const QRectF& fromCrop,
|
|
uint32_t toTex,
|
|
float toFlip,
|
|
const QRectF& toCrop,
|
|
qreal progress,
|
|
const QSize& size) {
|
|
glUseProgram(program.id);
|
|
glDisable(GL_BLEND);
|
|
|
|
glActiveTexture(GL_TEXTURE0);
|
|
glBindTexture(GL_TEXTURE_2D, fromTex);
|
|
glUniform1i(GLint(program.from), 0);
|
|
|
|
glActiveTexture(GL_TEXTURE1);
|
|
glBindTexture(GL_TEXTURE_2D, toTex);
|
|
glUniform1i(GLint(program.to), 1);
|
|
|
|
glUniform2f(GLint(program.size), float(size.width()), float(size.height()));
|
|
glUniform4f(
|
|
GLint(program.fromCrop),
|
|
GLfloat(fromCrop.x()),
|
|
GLfloat(fromCrop.y()),
|
|
GLfloat(fromCrop.width()),
|
|
GLfloat(fromCrop.height()));
|
|
glUniform4f(
|
|
GLint(program.toCrop),
|
|
GLfloat(toCrop.x()),
|
|
GLfloat(toCrop.y()),
|
|
GLfloat(toCrop.width()),
|
|
GLfloat(toCrop.height()));
|
|
glUniform1f(GLint(program.fromFlip), fromFlip);
|
|
glUniform1f(GLint(program.toFlip), toFlip);
|
|
glUniform1f(GLint(program.progress), float(progress));
|
|
|
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
}
|
|
|
|
uint32_t WallpaperService::targetTexFor(
|
|
const TransitionLayer& layer, int index) const {
|
|
if (layer.isVideo) return m_captureTex;
|
|
if (layer.tex != 0) return layer.tex;
|
|
Q_UNUSED(index);
|
|
return m_texture;
|
|
}
|
|
|
|
QRectF WallpaperService::cropForLayer(
|
|
const TransitionLayer& layer,
|
|
const QString& screen,
|
|
const QSize& size) const {
|
|
if (layer.isVideo) return QRectF(0.0, 0.0, 1.0, 1.0);
|
|
if (layer.tex != 0)
|
|
return sourceRect(layer.crops, screen, size, layer.image.size());
|
|
return sourceRect(m_crops, screen, size, m_image.size());
|
|
}
|
|
|
|
void WallpaperService::captureBase(
|
|
const QSize& size,
|
|
const QString& screen,
|
|
uint32_t oldTex,
|
|
float oldFlip,
|
|
const QRectF& oldCrop) {
|
|
Q_UNUSED(screen);
|
|
|
|
ensureBaseFbo();
|
|
if (m_baseTex == 0) return;
|
|
|
|
Q_UNUSED(oldCrop);
|
|
if (!m_baseImage.isNull()) {
|
|
uploadTexture(m_baseTex, m_baseImage);
|
|
m_baseSize = m_baseImage.size();
|
|
m_baseReady = true;
|
|
return;
|
|
}
|
|
|
|
if (m_baseSize != size) {
|
|
glBindTexture(GL_TEXTURE_2D, m_baseTex);
|
|
glTexImage2D(
|
|
GL_TEXTURE_2D,
|
|
0,
|
|
GL_RGBA8,
|
|
size.width(),
|
|
size.height(),
|
|
0,
|
|
GL_RGBA,
|
|
GL_UNSIGNED_BYTE,
|
|
nullptr);
|
|
m_baseSize = size;
|
|
}
|
|
|
|
bindRtt(m_baseFbo, m_baseTex, size);
|
|
drawTransitionPair(
|
|
m_neutralProgram,
|
|
oldTex,
|
|
oldFlip,
|
|
QRectF(0.0, 0.0, 1.0, 1.0),
|
|
oldTex,
|
|
oldFlip,
|
|
QRectF(0.0, 0.0, 1.0, 1.0),
|
|
1.0,
|
|
size);
|
|
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
m_baseReady = true;
|
|
}
|
|
|
|
void WallpaperService::renderComposite(
|
|
const QSize& size,
|
|
const QString& screen,
|
|
uint32_t outFbo,
|
|
uint32_t outTex,
|
|
int first,
|
|
int last,
|
|
uint32_t videoTex,
|
|
uint32_t posterTex) {
|
|
if (m_baseTex == 0) return;
|
|
|
|
ensureAccum(size);
|
|
|
|
float fromFlip = 0.0f;
|
|
QRectF fromCrop = QRectF(0.0, 0.0, 1.0, 1.0);
|
|
if (!m_baseImage.isNull()) {
|
|
fromFlip = 1.0f;
|
|
fromCrop = sourceRect(m_baseCrops, screen, size, m_baseImage.size());
|
|
}
|
|
|
|
bindRtt(m_accumFbo, m_accumA, size);
|
|
drawTransitionPair(
|
|
m_neutralProgram,
|
|
m_baseTex,
|
|
fromFlip,
|
|
fromCrop,
|
|
m_baseTex,
|
|
fromFlip,
|
|
fromCrop,
|
|
1.0,
|
|
size);
|
|
|
|
uint32_t read = m_accumA;
|
|
uint32_t write = m_accumB;
|
|
int drawn = 0;
|
|
|
|
for (int i = first; i < last; ++i) {
|
|
const TransitionLayer& layer = m_transitions[size_t(i)];
|
|
if (!layer.ready) continue;
|
|
|
|
uint32_t toTex = 0;
|
|
float toFlip = 1.0f;
|
|
QRectF toCrop(0.0, 0.0, 1.0, 1.0);
|
|
|
|
if (layer.isVideo) {
|
|
if (m_videoReady.load(std::memory_order_relaxed) && videoTex != 0) {
|
|
toTex = videoTex;
|
|
toFlip = 0.0f;
|
|
} else if (posterTex != 0) {
|
|
toTex = posterTex;
|
|
toFlip = 1.0f;
|
|
toCrop =
|
|
sourceRect(layer.crops, screen, size, m_posterImage.size());
|
|
} else {
|
|
toTex = videoTex;
|
|
toFlip = 0.0f;
|
|
}
|
|
} else {
|
|
toTex = targetTexFor(layer, i);
|
|
toCrop = cropForLayer(layer, screen, size);
|
|
}
|
|
|
|
if (toTex == 0) continue;
|
|
|
|
const TransProgram program = layer.transitionFile.isEmpty()
|
|
? m_neutralProgram
|
|
: programFor(layer.transitionFile);
|
|
|
|
bindRtt(m_accumFbo, write, size);
|
|
drawTransitionPair(
|
|
program,
|
|
read,
|
|
0.0f,
|
|
QRectF(0.0, 0.0, 1.0, 1.0),
|
|
toTex,
|
|
toFlip,
|
|
toCrop,
|
|
layer.progress,
|
|
size);
|
|
|
|
std::swap(read, write);
|
|
++drawn;
|
|
}
|
|
|
|
if (drawn == 0) {
|
|
if (outFbo == m_baseFbo) return;
|
|
read = m_accumA;
|
|
}
|
|
|
|
if (outFbo == 0) {
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
glViewport(0, 0, size.width(), size.height());
|
|
} else {
|
|
bindRtt(outFbo, outTex, size);
|
|
}
|
|
|
|
drawTransitionPair(
|
|
m_neutralProgram,
|
|
read,
|
|
0.0f,
|
|
QRectF(0.0, 0.0, 1.0, 1.0),
|
|
read,
|
|
0.0f,
|
|
QRectF(0.0, 0.0, 1.0, 1.0),
|
|
1.0,
|
|
size);
|
|
}
|
|
|
|
void WallpaperService::drawSurfaceTransition(WallpaperSurface* surface) {
|
|
if (!surface->needsDraw() || !surface->frameReady()) return;
|
|
|
|
EGLSurface target = surface->eglSurface();
|
|
if (m_context == EGL_NO_CONTEXT || target == EGL_NO_SURFACE ||
|
|
surface->bufferSize().isEmpty())
|
|
return;
|
|
|
|
if (!eglMakeCurrent(m_eglDisplay, target, target, m_context)) {
|
|
qWarning(wpLog) << "WallpaperService: the context cannot be made "
|
|
"current, eglGetError = 0x"
|
|
<< Qt::hex << unsigned(eglGetError());
|
|
return;
|
|
}
|
|
|
|
eglSwapInterval(m_eglDisplay, 0);
|
|
|
|
if (m_vao == 0) {
|
|
glGenVertexArrays(1, &m_vao);
|
|
glBindVertexArray(m_vao);
|
|
}
|
|
|
|
struct ContextRelease {
|
|
EGLDisplay display;
|
|
~ContextRelease() {
|
|
eglMakeCurrent(
|
|
display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
|
}
|
|
} contextRelease{m_eglDisplay};
|
|
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
|
|
const QSize size = surface->bufferSize();
|
|
if (size.isEmpty()) return;
|
|
const QString screen = surface->screen();
|
|
|
|
if (m_transitions.empty()) return;
|
|
|
|
if (!ensureTransitionProgram()) return;
|
|
if (m_blackTex == 0) createBlackTexture(m_blackTex);
|
|
|
|
ensureBaseFbo();
|
|
|
|
const bool newestVideoLive =
|
|
!m_transitions.empty() && m_transitions.back().isVideo &&
|
|
m_videoReady.load(std::memory_order_relaxed);
|
|
|
|
bool anyVideoLayer = false;
|
|
for (const auto& layer : m_transitions)
|
|
if (layer.isVideo) {
|
|
anyVideoLayer = true;
|
|
break;
|
|
}
|
|
|
|
bool needVideo =
|
|
(m_needCaptureBase && m_baseIsVideo) ||
|
|
(m_videoReady.load(std::memory_order_relaxed) && anyVideoLayer);
|
|
|
|
const uint32_t videoTex = needVideo ? renderVideoIntoTexture(size) : 0;
|
|
|
|
if (m_needCaptureBase && m_baseIsVideo && videoTex == 0) return;
|
|
|
|
if (m_needCaptureBase) {
|
|
uint32_t oldTex = 0;
|
|
float oldFlip = 0.0f;
|
|
QRectF oldCrop(0.0, 0.0, 1.0, 1.0);
|
|
|
|
if (m_baseIsBlack) {
|
|
oldTex = m_blackTex;
|
|
oldFlip = 1.0f;
|
|
} else if (m_baseIsVideo) {
|
|
oldTex = videoTex;
|
|
oldFlip = 0.0f;
|
|
} else {
|
|
oldTex = m_texture;
|
|
oldFlip = 1.0f;
|
|
oldCrop =
|
|
sourceRect(m_baseCrops, screen, size, m_baseImage.size());
|
|
}
|
|
|
|
if (oldTex != 0)
|
|
captureBase(size, screen, oldTex, oldFlip, oldCrop);
|
|
}
|
|
|
|
if (!m_transitions.back().isVideo) {
|
|
const bool upload = m_imageChanged;
|
|
m_imageChanged = false;
|
|
if (m_texture == 0 || upload) uploadImage();
|
|
} else {
|
|
m_imageChanged = false;
|
|
}
|
|
|
|
if (m_isVideo && m_posterChanged) {
|
|
m_posterChanged = false;
|
|
if (!m_posterImage.isNull())
|
|
uploadTexture(m_posterTex, m_posterImage);
|
|
}
|
|
|
|
const bool newestVideoPoster = newestVideoLive == false &&
|
|
m_transitions.back().isVideo &&
|
|
m_posterReady && m_posterTex != 0;
|
|
|
|
const bool newestReady =
|
|
m_isVideo
|
|
? (newestVideoLive || newestVideoPoster)
|
|
: (!m_image.isNull() && m_texture != 0 && m_replacementReady);
|
|
|
|
for (size_t i = 0; i < m_transitions.size(); ++i) {
|
|
TransitionLayer& layer = m_transitions[i];
|
|
const bool ready =
|
|
(i + 1 == m_transitions.size())
|
|
? newestReady
|
|
: (layer.isVideo
|
|
? (layer.ready || m_videoReady || m_posterReady)
|
|
: layer.tex != 0);
|
|
if (!ready) continue;
|
|
|
|
layer.ready = true;
|
|
if (!layer.started) {
|
|
layer.started = true;
|
|
layer.timer.start();
|
|
}
|
|
layer.progress =
|
|
qreal(layer.timer.elapsed()) / qreal(kFadeDurationMs);
|
|
if (layer.progress > 1.0) layer.progress = 1.0;
|
|
}
|
|
|
|
const bool newestSettled =
|
|
m_transitions.back().isVideo ? newestVideoLive : true;
|
|
|
|
const bool collapse = m_collapseVideo && m_transitions.size() > 1;
|
|
const bool promote = m_promoteOldest && !m_transitions.empty();
|
|
|
|
if (collapse) {
|
|
ensureAccum(size);
|
|
renderComposite(
|
|
size,
|
|
screen,
|
|
m_baseFbo,
|
|
m_baseTex,
|
|
0,
|
|
int(m_transitions.size()) - 1,
|
|
videoTex,
|
|
m_posterTex);
|
|
} else if (promote) {
|
|
ensureAccum(size);
|
|
renderComposite(
|
|
size, screen, m_baseFbo, m_baseTex, 0, 1, videoTex, m_posterTex);
|
|
}
|
|
|
|
int first = 0;
|
|
if (collapse)
|
|
first = int(m_transitions.size()) - 1;
|
|
else if (promote)
|
|
first = 1;
|
|
|
|
const int last = int(m_transitions.size());
|
|
const TransitionLayer& newest = m_transitions.back();
|
|
|
|
if (m_baseIsBlack) {
|
|
const qreal p = newest.ready ? newest.progress : 0.0;
|
|
const float a = p < 0.0 ? 0.0f : (p > 1.0 ? 1.0f : float(p));
|
|
const float w = float(size.width());
|
|
const float h = float(size.height());
|
|
|
|
if (m_program == 0) m_program = createProgram();
|
|
if (m_program == 0) return;
|
|
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
glViewport(0, 0, size.width(), size.height());
|
|
|
|
if (m_isVideo) {
|
|
if (!m_videoReady.load(std::memory_order_relaxed)) {
|
|
if (!m_posterReady || m_posterTex == 0) return;
|
|
|
|
glUseProgram(m_program);
|
|
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
glEnable(GL_BLEND);
|
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
|
|
drawTexture(
|
|
m_posterTex,
|
|
sourceRect(m_crops, screen, size, m_posterImage.size()),
|
|
w,
|
|
h,
|
|
a);
|
|
} else {
|
|
glDisable(GL_BLEND);
|
|
|
|
#ifdef ZHAVE_FFMPEG
|
|
if (m_decoder) {
|
|
if (m_decoder->textureId() != 0) {
|
|
if (m_videoProgram == 0)
|
|
m_videoProgram = createVideoProgram();
|
|
|
|
if (m_videoProgram != 0) {
|
|
glUseProgram(m_videoProgram);
|
|
const QSize src = m_decoder->frameSize();
|
|
drawVideoTexture(
|
|
m_decoder->textureId(),
|
|
m_decoder->chromaTextureId(),
|
|
m_decoder->colorMatrix(),
|
|
m_decoder->colorRange(),
|
|
QRectF(0.0, 0.0, 1.0, 1.0),
|
|
w,
|
|
h,
|
|
src.width() > 0 ? float(src.width()) : w,
|
|
src.height() > 0 ? float(src.height()) : h,
|
|
1.0f);
|
|
}
|
|
}
|
|
} else
|
|
#endif
|
|
{
|
|
if (!m_mpvRender) initMpvRender();
|
|
if (!m_mpvRender) return;
|
|
renderMpvToSurface(size);
|
|
}
|
|
|
|
glEnable(GL_BLEND);
|
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
glUseProgram(m_program);
|
|
|
|
if (m_blackTex != 0)
|
|
drawTexture(
|
|
m_blackTex,
|
|
QRectF(0.0, 0.0, 1.0, 1.0),
|
|
w,
|
|
h,
|
|
1.0f - a);
|
|
}
|
|
} else {
|
|
if (m_texture == 0 || m_image.isNull()) return;
|
|
|
|
glUseProgram(m_program);
|
|
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
glEnable(GL_BLEND);
|
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
|
|
drawTexture(
|
|
m_texture,
|
|
sourceRect(m_crops, screen, size, m_image.size()),
|
|
w,
|
|
h,
|
|
a);
|
|
}
|
|
|
|
if (newest.ready && newest.progress >= 1.0 && newestSettled)
|
|
m_newestDone = true;
|
|
} else {
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
glViewport(0, 0, size.width(), size.height());
|
|
|
|
if (!m_baseReady) {
|
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
} else {
|
|
renderComposite(
|
|
size, screen, 0, 0, first, last, videoTex, m_posterTex);
|
|
}
|
|
|
|
if (newest.ready && newest.progress >= 1.0 && newestSettled)
|
|
m_newestDone = true;
|
|
}
|
|
}
|
|
|
|
surface->requestFrame();
|
|
|
|
if (!eglSwapBuffers(m_eglDisplay, target)) {
|
|
qWarning(wpLog)
|
|
<< "WallpaperService: eglSwapBuffers failed, eglGetError = 0x"
|
|
<< QString::number(unsigned(eglGetError()), 16);
|
|
return;
|
|
}
|
|
|
|
if ((m_isVideo || m_baseIsVideo) && m_mpvRender)
|
|
mpv_render_context_report_swap(m_mpvRender);
|
|
|
|
surface->endDraw();
|
|
}
|
|
|
|
void WallpaperService::addSurface(const QString& screen) {
|
|
if (!m_layerShell || !m_viewporter || !m_scaleManager) return;
|
|
|
|
QScreen* qscreen = nullptr;
|
|
for (auto* candidate : QGuiApplication::screens()) {
|
|
if (candidate->name() == screen) {
|
|
qscreen = candidate;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!qscreen) {
|
|
qWarning(wpLog) << "WallpaperService:" << screen << "is not a screen";
|
|
return;
|
|
}
|
|
|
|
auto* waylandScreen =
|
|
dynamic_cast<QtWaylandClient::QWaylandScreen*>(qscreen->handle());
|
|
if (!waylandScreen || !waylandScreen->output()) {
|
|
qWarning(wpLog) << "WallpaperService:" << screen
|
|
<< "is not a Wayland screen";
|
|
return;
|
|
}
|
|
|
|
wl_surface* wlSurface = wl_compositor_create_surface(m_compositor);
|
|
if (!wlSurface) return;
|
|
|
|
auto* surface =
|
|
new WallpaperSurface(this, wlSurface, waylandScreen->output(), screen);
|
|
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
m_surfaces.append(surface);
|
|
}
|
|
|
|
notifyBufferSizes();
|
|
}
|
|
|
|
} // namespace ZShell::wallpaper
|