2024-02-18 23:08:03 +00:00
|
|
|
#include "Background.hpp"
|
|
|
|
|
#include "../Renderer.hpp"
|
2025-10-08 10:45:09 +02:00
|
|
|
#include "../AsyncResourceManager.hpp"
|
2025-06-22 09:24:39 +02:00
|
|
|
#include "../Framebuffer.hpp"
|
2024-12-18 16:28:05 +01:00
|
|
|
#include "../../core/hyprlock.hpp"
|
2024-12-26 15:43:11 +00:00
|
|
|
#include "../../helpers/Log.hpp"
|
|
|
|
|
#include "../../helpers/MiscFunctions.hpp"
|
2025-06-22 09:24:39 +02:00
|
|
|
#include "../../core/AnimationManager.hpp"
|
|
|
|
|
#include "../../config/ConfigManager.hpp"
|
2026-03-02 16:37:41 +00:00
|
|
|
#include <algorithm>
|
2024-12-18 16:28:05 +01:00
|
|
|
#include <chrono>
|
2024-10-13 12:04:32 +00:00
|
|
|
#include <hyprlang.hpp>
|
2024-12-18 16:28:05 +01:00
|
|
|
#include <filesystem>
|
2026-03-02 16:37:41 +00:00
|
|
|
#include <unordered_set>
|
2024-12-18 16:28:05 +01:00
|
|
|
#include <GLES3/gl32.h>
|
|
|
|
|
|
2025-06-22 09:24:39 +02:00
|
|
|
CBackground::CBackground() {
|
|
|
|
|
blurredFB = makeUnique<CFramebuffer>();
|
|
|
|
|
pendingBlurredFB = makeUnique<CFramebuffer>();
|
2025-07-25 18:39:56 +02:00
|
|
|
transformedScFB = makeUnique<CFramebuffer>();
|
2025-06-22 09:24:39 +02:00
|
|
|
}
|
|
|
|
|
|
2024-12-18 16:28:05 +01:00
|
|
|
CBackground::~CBackground() {
|
2025-03-05 08:35:43 +01:00
|
|
|
reset();
|
|
|
|
|
}
|
2024-12-18 16:28:05 +01:00
|
|
|
|
2025-06-28 11:01:28 +02:00
|
|
|
void CBackground::registerSelf(const ASP<CBackground>& self) {
|
2025-03-05 08:35:43 +01:00
|
|
|
m_self = self;
|
2024-12-18 16:28:05 +01:00
|
|
|
}
|
2024-02-18 23:08:03 +00:00
|
|
|
|
2026-02-20 19:49:58 +03:00
|
|
|
static std::string runAndGetPath(const std::string& reloadCommand) {
|
|
|
|
|
std::string path = spawnSync(reloadCommand);
|
|
|
|
|
|
|
|
|
|
if (path.ends_with('\0'))
|
|
|
|
|
path.pop_back();
|
|
|
|
|
|
|
|
|
|
if (path.ends_with('\n'))
|
|
|
|
|
path.pop_back();
|
|
|
|
|
|
|
|
|
|
if (path.starts_with("file://"))
|
|
|
|
|
path = path.substr(7);
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-02 16:37:41 +00:00
|
|
|
// ── Video support ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
SVideoState::~SVideoState() {
|
|
|
|
|
running = false;
|
|
|
|
|
if (decodeThread.joinable())
|
|
|
|
|
decodeThread.join();
|
|
|
|
|
if (swsCtx) sws_freeContext(swsCtx);
|
|
|
|
|
if (codecCtx) avcodec_free_context(&codecCtx);
|
|
|
|
|
if (formatCtx) avformat_close_input(&formatCtx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CBackground::isVideoFile(const std::string& path) {
|
|
|
|
|
static const std::unordered_set<std::string> VIDEO_EXT = {
|
|
|
|
|
".mp4", ".mkv", ".webm", ".avi", ".mov", ".m4v",
|
|
|
|
|
".flv", ".wmv", ".ts", ".m2ts", ".gif"
|
|
|
|
|
};
|
|
|
|
|
auto ext = std::filesystem::path(path).extension().string();
|
|
|
|
|
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
|
|
|
|
|
return VIDEO_EXT.count(ext) > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CBackground::openVideo(const std::string& path) {
|
|
|
|
|
auto& v = *m_video;
|
|
|
|
|
|
|
|
|
|
if (avformat_open_input(&v.formatCtx, path.c_str(), nullptr, nullptr) < 0) {
|
|
|
|
|
Debug::log(ERR, "CBackground: avformat_open_input failed for {}", path);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (avformat_find_stream_info(v.formatCtx, nullptr) < 0) {
|
|
|
|
|
Debug::log(ERR, "CBackground: avformat_find_stream_info failed for {}", path);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const AVCodec* codec = nullptr;
|
|
|
|
|
v.streamIdx = av_find_best_stream(v.formatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
|
|
|
|
|
if (v.streamIdx < 0 || !codec) {
|
|
|
|
|
Debug::log(ERR, "CBackground: no video stream found in {}", path);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
v.codecCtx = avcodec_alloc_context3(codec);
|
|
|
|
|
if (!v.codecCtx) {
|
|
|
|
|
Debug::log(ERR, "CBackground: avcodec_alloc_context3 failed");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
avcodec_parameters_to_context(v.codecCtx, v.formatCtx->streams[v.streamIdx]->codecpar);
|
|
|
|
|
|
|
|
|
|
if (avcodec_open2(v.codecCtx, codec, nullptr) < 0) {
|
|
|
|
|
Debug::log(ERR, "CBackground: avcodec_open2 failed for {}", path);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
v.frameW = v.codecCtx->width;
|
|
|
|
|
v.frameH = v.codecCtx->height;
|
|
|
|
|
v.timeBase = av_q2d(v.formatCtx->streams[v.streamIdx]->time_base);
|
|
|
|
|
|
|
|
|
|
// swsCtx is created lazily per-frame via sws_getCachedContext so that
|
|
|
|
|
// we handle codecs where pix_fmt is only known after the first decode.
|
|
|
|
|
v.frameData.resize(4 * v.frameW * v.frameH);
|
|
|
|
|
Debug::log(LOG, "CBackground: opened video {} ({}x{}, timebase={:.6f})",
|
|
|
|
|
path, v.frameW, v.frameH, v.timeBase);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CBackground::startVideoThread() {
|
|
|
|
|
auto& v = *m_video;
|
|
|
|
|
v.running = true;
|
|
|
|
|
v.startTime = std::chrono::steady_clock::now();
|
|
|
|
|
|
|
|
|
|
v.decodeThread = std::thread([&v]() {
|
|
|
|
|
AVPacket* pkt = av_packet_alloc();
|
|
|
|
|
AVFrame* frame = av_frame_alloc();
|
|
|
|
|
std::vector<uint8_t> tmpBuf(4 * v.frameW * v.frameH);
|
|
|
|
|
|
|
|
|
|
while (v.running) {
|
|
|
|
|
int ret = av_read_frame(v.formatCtx, pkt);
|
|
|
|
|
|
|
|
|
|
if (ret == AVERROR_EOF) {
|
|
|
|
|
// Flush decoder's internal buffer
|
|
|
|
|
avcodec_send_packet(v.codecCtx, nullptr);
|
|
|
|
|
while (avcodec_receive_frame(v.codecCtx, frame) == 0)
|
|
|
|
|
av_frame_unref(frame);
|
|
|
|
|
|
|
|
|
|
// Loop: seek back to beginning
|
|
|
|
|
av_seek_frame(v.formatCtx, v.streamIdx, 0, AVSEEK_FLAG_BACKWARD);
|
|
|
|
|
avcodec_flush_buffers(v.codecCtx);
|
|
|
|
|
v.startTime = std::chrono::steady_clock::now();
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ret < 0)
|
|
|
|
|
break; // unrecoverable error
|
|
|
|
|
|
|
|
|
|
if (pkt->stream_index != v.streamIdx) {
|
|
|
|
|
av_packet_unref(pkt);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (avcodec_send_packet(v.codecCtx, pkt) < 0) {
|
|
|
|
|
av_packet_unref(pkt);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
av_packet_unref(pkt);
|
|
|
|
|
|
|
|
|
|
while (avcodec_receive_frame(v.codecCtx, frame) == 0) {
|
|
|
|
|
if (!v.running)
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
// Lazily create / update SwsContext to match the frame's actual
|
|
|
|
|
// pixel format (some codecs only report it after the first frame).
|
|
|
|
|
v.swsCtx = sws_getCachedContext(v.swsCtx,
|
|
|
|
|
frame->width, frame->height, (AVPixelFormat)frame->format,
|
|
|
|
|
v.frameW, v.frameH, AV_PIX_FMT_RGBA,
|
|
|
|
|
SWS_BILINEAR, nullptr, nullptr, nullptr);
|
|
|
|
|
if (!v.swsCtx) {
|
|
|
|
|
av_frame_unref(frame);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// sws_scale requires 4-element pointer/stride arrays even for
|
|
|
|
|
// packed formats — passing a 1-element array causes UB reads.
|
|
|
|
|
uint8_t* dst[4] = {tmpBuf.data(), nullptr, nullptr, nullptr};
|
|
|
|
|
int stride[4] = {4 * v.frameW, 0, 0, 0};
|
|
|
|
|
sws_scale(v.swsCtx,
|
|
|
|
|
(const uint8_t* const*)frame->data, frame->linesize,
|
|
|
|
|
0, frame->height, dst, stride);
|
|
|
|
|
|
|
|
|
|
// Publish frame via O(1) swap (no memcpy)
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(v.frameMutex);
|
|
|
|
|
std::swap(v.frameData, tmpBuf);
|
|
|
|
|
v.hasNewFrame = true;
|
|
|
|
|
}
|
|
|
|
|
// tmpBuf now holds old frame data and will be overwritten next iteration
|
|
|
|
|
|
|
|
|
|
// PTS-based frame pacing
|
|
|
|
|
if (frame->pts != AV_NOPTS_VALUE) {
|
|
|
|
|
double pts_sec = frame->pts * v.timeBase;
|
|
|
|
|
auto target = v.startTime +
|
|
|
|
|
std::chrono::duration_cast<std::chrono::steady_clock::duration>(
|
|
|
|
|
std::chrono::duration<double>(pts_sec));
|
|
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
|
|
// Safety cap: never sleep > 5 s (guards against bogus PTS values)
|
|
|
|
|
auto maxTarget = now + std::chrono::seconds(5);
|
|
|
|
|
if (target > now && target < maxTarget)
|
|
|
|
|
std::this_thread::sleep_until(target);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
av_frame_unref(frame);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
av_packet_free(&pkt);
|
|
|
|
|
av_frame_free(&frame);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CBackground::stopVideo() {
|
|
|
|
|
m_video.reset(); // ~SVideoState(): sets running=false, joins thread, frees ffmpeg
|
|
|
|
|
m_videoTexture.destroyTexture();
|
|
|
|
|
m_uploadBuffer.clear();
|
|
|
|
|
m_isVideo = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── End video support ─────────────────────────────────────────────────────────
|
|
|
|
|
|
2025-03-05 08:35:43 +01:00
|
|
|
void CBackground::configure(const std::unordered_map<std::string, std::any>& props, const SP<COutput>& pOutput) {
|
|
|
|
|
reset();
|
2024-02-21 22:19:01 +00:00
|
|
|
|
2024-12-18 16:28:05 +01:00
|
|
|
try {
|
|
|
|
|
color = std::any_cast<Hyprlang::INT>(props.at("color"));
|
|
|
|
|
blurPasses = std::any_cast<Hyprlang::INT>(props.at("blur_passes"));
|
|
|
|
|
blurSize = std::any_cast<Hyprlang::INT>(props.at("blur_size"));
|
|
|
|
|
vibrancy = std::any_cast<Hyprlang::FLOAT>(props.at("vibrancy"));
|
|
|
|
|
vibrancy_darkness = std::any_cast<Hyprlang::FLOAT>(props.at("vibrancy_darkness"));
|
|
|
|
|
noise = std::any_cast<Hyprlang::FLOAT>(props.at("noise"));
|
|
|
|
|
brightness = std::any_cast<Hyprlang::FLOAT>(props.at("brightness"));
|
|
|
|
|
contrast = std::any_cast<Hyprlang::FLOAT>(props.at("contrast"));
|
|
|
|
|
path = std::any_cast<Hyprlang::STRING>(props.at("path"));
|
|
|
|
|
reloadCommand = std::any_cast<Hyprlang::STRING>(props.at("reload_cmd"));
|
|
|
|
|
reloadTime = std::any_cast<Hyprlang::INT>(props.at("reload_time"));
|
|
|
|
|
|
|
|
|
|
} catch (const std::bad_any_cast& e) {
|
|
|
|
|
RASSERT(false, "Failed to construct CBackground: {}", e.what()); //
|
|
|
|
|
} catch (const std::out_of_range& e) {
|
|
|
|
|
RASSERT(false, "Missing propperty for CBackground: {}", e.what()); //
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-05 08:35:43 +01:00
|
|
|
isScreenshot = path == "screenshot";
|
|
|
|
|
|
2025-06-22 09:24:39 +02:00
|
|
|
viewport = pOutput->getViewport();
|
|
|
|
|
outputPort = pOutput->stringPort;
|
2025-07-25 18:39:56 +02:00
|
|
|
transform = wlTransformToHyprutils(invertTransform(pOutput->transform));
|
2025-10-08 10:45:09 +02:00
|
|
|
scResourceID = CAsyncResourceManager::resourceIDForScreencopy(pOutput->stringPort);
|
2025-06-22 09:24:39 +02:00
|
|
|
|
|
|
|
|
g_pAnimationManager->createAnimation(0.f, crossFadeProgress, g_pConfigManager->m_AnimationTree.getConfig("fadeIn"));
|
2025-03-05 08:35:43 +01:00
|
|
|
|
2025-10-08 10:45:09 +02:00
|
|
|
if (!g_asyncResourceManager->checkIdPresent(scResourceID)) {
|
2025-07-25 18:39:56 +02:00
|
|
|
Debug::log(LOG, "Missing screenshot for output {}", outputPort);
|
2025-10-08 10:45:09 +02:00
|
|
|
scResourceID = 0;
|
2025-07-25 18:39:56 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-20 19:49:58 +03:00
|
|
|
if (!reloadCommand.empty() && path.empty())
|
|
|
|
|
path = runAndGetPath(reloadCommand);
|
|
|
|
|
|
2025-03-05 08:35:43 +01:00
|
|
|
if (isScreenshot) {
|
2025-10-08 10:45:09 +02:00
|
|
|
resourceID = scResourceID; // Fallback to solid background:color when scResourceID==0
|
2025-03-05 08:35:43 +01:00
|
|
|
|
|
|
|
|
if (!g_pHyprlock->getScreencopy()) {
|
|
|
|
|
Debug::log(ERR, "No screencopy support! path=screenshot won't work. Falling back to background color.");
|
2025-10-08 10:45:09 +02:00
|
|
|
resourceID = 0;
|
2025-03-05 08:35:43 +01:00
|
|
|
}
|
2026-03-02 16:37:41 +00:00
|
|
|
} else if (!path.empty()) {
|
|
|
|
|
if (isVideoFile(path)) {
|
|
|
|
|
m_isVideo = true;
|
|
|
|
|
m_video = makeUnique<SVideoState>();
|
|
|
|
|
if (!openVideo(path)) {
|
|
|
|
|
Debug::log(ERR, "CBackground: failed to open '{}' as video, falling back to image", path);
|
|
|
|
|
m_video.reset();
|
|
|
|
|
m_isVideo = false;
|
|
|
|
|
resourceID = g_asyncResourceManager->requestImage(path, m_imageRevision, nullptr);
|
|
|
|
|
} else {
|
|
|
|
|
// Pre-size the upload buffer so it's never empty when the main
|
|
|
|
|
// thread swaps it with frameData. An empty vector has data()==null
|
|
|
|
|
// which would propagate back to tmpBuf in the decode thread and
|
|
|
|
|
// cause "bad dst image pointers" on the third sws_scale call.
|
|
|
|
|
m_uploadBuffer.resize(4 * m_video->frameW * m_video->frameH);
|
|
|
|
|
startVideoThread();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
resourceID = g_asyncResourceManager->requestImage(path, m_imageRevision, nullptr);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-05 08:35:43 +01:00
|
|
|
|
2026-03-02 16:37:41 +00:00
|
|
|
if (!reloadCommand.empty() && reloadTime > -1 && !m_isVideo) {
|
2024-12-26 15:43:11 +00:00
|
|
|
try {
|
2025-10-29 05:20:34 +11:00
|
|
|
if (!isScreenshot)
|
|
|
|
|
modificationTime = std::filesystem::last_write_time(absolutePath(path, ""));
|
2024-12-26 15:43:11 +00:00
|
|
|
} catch (std::exception& e) { Debug::log(ERR, "{}", e.what()); }
|
2024-12-18 16:28:05 +01:00
|
|
|
|
2025-10-29 05:20:34 +11:00
|
|
|
plantReloadTimer(); // No reloads if reloadCommand is empty
|
2024-12-26 15:43:11 +00:00
|
|
|
}
|
2024-02-18 23:08:03 +00:00
|
|
|
}
|
|
|
|
|
|
2025-03-05 08:35:43 +01:00
|
|
|
void CBackground::reset() {
|
2026-03-02 16:37:41 +00:00
|
|
|
if (m_isVideo)
|
|
|
|
|
stopVideo();
|
|
|
|
|
|
2025-03-05 08:35:43 +01:00
|
|
|
if (reloadTimer) {
|
|
|
|
|
reloadTimer->cancel();
|
|
|
|
|
reloadTimer.reset();
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-22 09:24:39 +02:00
|
|
|
blurredFB->destroyBuffer();
|
|
|
|
|
pendingBlurredFB->destroyBuffer();
|
2025-03-05 08:35:43 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-25 18:39:56 +02:00
|
|
|
void CBackground::updatePrimaryAsset() {
|
2025-10-08 10:45:09 +02:00
|
|
|
if (asset || resourceID == 0)
|
2025-07-25 18:39:56 +02:00
|
|
|
return;
|
|
|
|
|
|
2025-10-08 10:45:09 +02:00
|
|
|
asset = g_asyncResourceManager->getAssetByID(resourceID);
|
2025-07-25 18:39:56 +02:00
|
|
|
if (!asset)
|
|
|
|
|
return;
|
|
|
|
|
|
2025-10-08 10:45:09 +02:00
|
|
|
const bool NEEDFB = (isScreenshot || blurPasses > 0 || asset->m_vSize != viewport || transform != HYPRUTILS_TRANSFORM_NORMAL) && (!blurredFB->isAllocated() || firstRender);
|
2025-07-25 18:39:56 +02:00
|
|
|
if (NEEDFB)
|
2025-10-08 10:45:09 +02:00
|
|
|
renderToFB(*asset, *blurredFB, blurPasses, isScreenshot);
|
2025-07-25 18:39:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CBackground::updatePendingAsset() {
|
|
|
|
|
// For crossfading a new asset
|
|
|
|
|
if (!pendingAsset || blurPasses == 0 || pendingBlurredFB->isAllocated())
|
|
|
|
|
return;
|
|
|
|
|
|
2025-10-08 10:45:09 +02:00
|
|
|
renderToFB(*pendingAsset, *pendingBlurredFB, blurPasses);
|
2025-07-25 18:39:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CBackground::updateScAsset() {
|
2025-10-08 10:45:09 +02:00
|
|
|
if (scAsset || scResourceID == 0)
|
2025-07-25 18:39:56 +02:00
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// path=screenshot -> scAsset = asset
|
2025-10-08 10:45:09 +02:00
|
|
|
scAsset = (asset && isScreenshot) ? asset : g_asyncResourceManager->getAssetByID(scResourceID);
|
2025-07-25 18:39:56 +02:00
|
|
|
if (!scAsset)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
const bool NEEDSCTRANSFORM = transform != HYPRUTILS_TRANSFORM_NORMAL;
|
|
|
|
|
if (NEEDSCTRANSFORM)
|
2025-10-08 10:45:09 +02:00
|
|
|
renderToFB(*scAsset, *transformedScFB, 0, true);
|
2025-07-25 18:39:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const CTexture& CBackground::getPrimaryAssetTex() const {
|
|
|
|
|
// This case is only for background:path=screenshot with blurPasses=0
|
|
|
|
|
if (isScreenshot && blurPasses == 0 && transformedScFB->isAllocated())
|
|
|
|
|
return transformedScFB->m_cTex;
|
|
|
|
|
|
2025-10-08 10:45:09 +02:00
|
|
|
return (blurredFB->isAllocated()) ? blurredFB->m_cTex : *asset;
|
2025-07-25 18:39:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const CTexture& CBackground::getPendingAssetTex() const {
|
2025-10-08 10:45:09 +02:00
|
|
|
return (pendingBlurredFB->isAllocated()) ? pendingBlurredFB->m_cTex : *pendingAsset;
|
2025-07-25 18:39:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const CTexture& CBackground::getScAssetTex() const {
|
2025-10-08 10:45:09 +02:00
|
|
|
return (transformedScFB->isAllocated()) ? transformedScFB->m_cTex : *scAsset;
|
2025-07-25 18:39:56 +02:00
|
|
|
}
|
|
|
|
|
|
2025-01-06 12:34:21 +00:00
|
|
|
void CBackground::renderRect(CHyprColor color) {
|
2024-07-07 18:43:17 +02:00
|
|
|
CBox monbox = {0, 0, viewport.x, viewport.y};
|
|
|
|
|
g_pRenderer->renderRect(monbox, color, 0);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-28 11:01:28 +02:00
|
|
|
static void onReloadTimer(AWP<CBackground> ref) {
|
2025-03-05 08:35:43 +01:00
|
|
|
if (auto PBG = ref.lock(); PBG) {
|
|
|
|
|
PBG->onReloadTimerUpdate();
|
|
|
|
|
PBG->plantReloadTimer();
|
|
|
|
|
}
|
2024-12-18 16:28:05 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-25 18:39:56 +02:00
|
|
|
static CBox getScaledBoxForTextureSize(const Vector2D& size, const Vector2D& viewport) {
|
2025-06-22 09:24:39 +02:00
|
|
|
CBox texbox = {{}, size};
|
2024-02-18 23:08:03 +00:00
|
|
|
|
2025-06-22 09:24:39 +02:00
|
|
|
float scaleX = viewport.x / size.x;
|
|
|
|
|
float scaleY = viewport.y / size.y;
|
2024-03-09 17:45:44 +01:00
|
|
|
|
2025-06-22 09:24:39 +02:00
|
|
|
texbox.w *= std::max(scaleX, scaleY);
|
|
|
|
|
texbox.h *= std::max(scaleX, scaleY);
|
2024-02-18 23:08:03 +00:00
|
|
|
|
2025-06-22 09:24:39 +02:00
|
|
|
if (scaleX > scaleY)
|
|
|
|
|
texbox.y = -(texbox.h - viewport.y) / 2.f;
|
|
|
|
|
else
|
|
|
|
|
texbox.x = -(texbox.w - viewport.x) / 2.f;
|
|
|
|
|
texbox.round();
|
2024-12-18 16:28:05 +01:00
|
|
|
|
2025-07-25 18:39:56 +02:00
|
|
|
return texbox;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CBackground::renderToFB(const CTexture& tex, CFramebuffer& fb, int passes, bool applyTransform) {
|
|
|
|
|
if (firstRender)
|
|
|
|
|
firstRender = false;
|
|
|
|
|
|
|
|
|
|
// make it brah
|
|
|
|
|
Vector2D size = tex.m_vSize;
|
|
|
|
|
if (applyTransform && transform % 2 == 1) {
|
|
|
|
|
size.x = tex.m_vSize.y;
|
|
|
|
|
size.y = tex.m_vSize.x;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto TEXBOX = getScaledBoxForTextureSize(size, viewport);
|
|
|
|
|
|
2025-06-22 09:24:39 +02:00
|
|
|
if (!fb.isAllocated())
|
|
|
|
|
fb.alloc(viewport.x, viewport.y); // TODO 10 bit
|
2024-12-18 16:28:05 +01:00
|
|
|
|
2025-06-22 09:24:39 +02:00
|
|
|
fb.bind();
|
2024-03-03 02:19:25 +00:00
|
|
|
|
2025-07-25 18:39:56 +02:00
|
|
|
g_pRenderer->renderTexture(TEXBOX, tex, 1.0, 0, applyTransform ? transform : HYPRUTILS_TRANSFORM_NORMAL);
|
2024-02-21 22:19:01 +00:00
|
|
|
|
2025-06-22 09:24:39 +02:00
|
|
|
if (blurPasses > 0)
|
|
|
|
|
g_pRenderer->blurFB(fb,
|
2025-07-25 18:39:56 +02:00
|
|
|
CRenderer::SBlurParams{
|
|
|
|
|
.size = blurSize,
|
|
|
|
|
.passes = passes,
|
|
|
|
|
.noise = noise,
|
|
|
|
|
.contrast = contrast,
|
|
|
|
|
.brightness = brightness,
|
|
|
|
|
.vibrancy = vibrancy,
|
|
|
|
|
.vibrancy_darkness = vibrancy_darkness,
|
|
|
|
|
});
|
2025-06-22 09:24:39 +02:00
|
|
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
|
|
|
|
}
|
2024-02-21 22:19:01 +00:00
|
|
|
|
2025-06-22 09:24:39 +02:00
|
|
|
bool CBackground::draw(const SRenderData& data) {
|
2026-03-02 16:37:41 +00:00
|
|
|
// ── Video background fast path ────────────────────────────────────────
|
|
|
|
|
if (m_isVideo && m_video) {
|
|
|
|
|
// Grab the latest decoded frame from the decode thread (O(1) swap)
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(m_video->frameMutex);
|
|
|
|
|
if (m_video->hasNewFrame) {
|
|
|
|
|
std::swap(m_uploadBuffer, m_video->frameData);
|
|
|
|
|
m_video->hasNewFrame = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Upload frame to GL texture
|
|
|
|
|
if (!m_uploadBuffer.empty()) {
|
|
|
|
|
if (!m_videoTexture.m_bAllocated) {
|
|
|
|
|
// First frame: allocate the GL texture
|
|
|
|
|
m_videoTexture.allocate();
|
|
|
|
|
glBindTexture(GL_TEXTURE_2D, m_videoTexture.m_iTexID);
|
|
|
|
|
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);
|
|
|
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
|
|
|
|
|
m_video->frameW, m_video->frameH, 0,
|
|
|
|
|
GL_RGBA, GL_UNSIGNED_BYTE, m_uploadBuffer.data());
|
|
|
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
|
|
|
m_videoTexture.m_vSize = {(double)m_video->frameW, (double)m_video->frameH};
|
|
|
|
|
m_videoTexture.m_iType = TEXTURE_RGBA;
|
|
|
|
|
m_videoTexture.m_iTarget = GL_TEXTURE_2D;
|
|
|
|
|
} else {
|
|
|
|
|
glBindTexture(GL_TEXTURE_2D, m_videoTexture.m_iTexID);
|
|
|
|
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0,
|
|
|
|
|
m_video->frameW, m_video->frameH,
|
|
|
|
|
GL_RGBA, GL_UNSIGNED_BYTE, m_uploadBuffer.data());
|
|
|
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Re-render blur FB if requested (GPU-side only, called every new frame)
|
|
|
|
|
if (blurPasses > 0)
|
|
|
|
|
renderToFB(m_videoTexture, *blurredFB, blurPasses);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render
|
|
|
|
|
if (!m_videoTexture.m_bAllocated) {
|
|
|
|
|
renderRect(color); // solid colour until first frame is ready
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const CTexture& TEX = (blurPasses > 0 && blurredFB->isAllocated())
|
|
|
|
|
? blurredFB->m_cTex : m_videoTexture;
|
|
|
|
|
const auto TEXBOX = getScaledBoxForTextureSize(TEX.m_vSize, viewport);
|
|
|
|
|
g_pRenderer->renderTexture(TEXBOX, TEX, data.opacity);
|
|
|
|
|
return true; // always request the next compositor frame
|
|
|
|
|
}
|
|
|
|
|
// ── End video path ────────────────────────────────────────────────────
|
|
|
|
|
|
2025-07-25 18:39:56 +02:00
|
|
|
updatePrimaryAsset();
|
|
|
|
|
updatePendingAsset();
|
|
|
|
|
updateScAsset();
|
2024-12-18 16:28:05 +01:00
|
|
|
|
2025-10-08 10:45:09 +02:00
|
|
|
if (asset && asset->m_iType == TEXTURE_INVALID) {
|
|
|
|
|
g_asyncResourceManager->unload(asset);
|
|
|
|
|
resourceID = 0;
|
2025-07-25 18:39:56 +02:00
|
|
|
renderRect(color);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-06-22 09:24:39 +02:00
|
|
|
|
2025-10-08 10:45:09 +02:00
|
|
|
if (!asset || resourceID == 0) {
|
2025-06-22 09:24:39 +02:00
|
|
|
// fade in/out with a solid color
|
|
|
|
|
if (data.opacity < 1.0 && scAsset) {
|
2025-07-25 18:39:56 +02:00
|
|
|
const auto& SCTEX = getScAssetTex();
|
|
|
|
|
const auto SCTEXBOX = getScaledBoxForTextureSize(SCTEX.m_vSize, viewport);
|
|
|
|
|
g_pRenderer->renderTexture(SCTEXBOX, SCTEX, 1, 0, HYPRUTILS_TRANSFORM_FLIPPED_180);
|
2025-06-22 09:24:39 +02:00
|
|
|
CHyprColor col = color;
|
|
|
|
|
col.a *= data.opacity;
|
|
|
|
|
renderRect(col);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2024-12-18 16:28:05 +01:00
|
|
|
|
2025-06-22 09:24:39 +02:00
|
|
|
renderRect(color);
|
2025-10-08 10:45:09 +02:00
|
|
|
return !asset && resourceID > 0; // resource not ready
|
2025-06-22 09:24:39 +02:00
|
|
|
}
|
2024-12-18 16:28:05 +01:00
|
|
|
|
2025-07-25 18:39:56 +02:00
|
|
|
const auto& TEX = getPrimaryAssetTex();
|
|
|
|
|
const auto TEXBOX = getScaledBoxForTextureSize(TEX.m_vSize, viewport);
|
|
|
|
|
if (data.opacity < 1.0 && scAsset) {
|
|
|
|
|
const auto& SCTEX = getScAssetTex();
|
|
|
|
|
g_pRenderer->renderTextureMix(TEXBOX, SCTEX, TEX, 1.0, data.opacity, 0);
|
|
|
|
|
} else if (crossFadeProgress->isBeingAnimated()) {
|
|
|
|
|
const auto& PENDINGTEX = getPendingAssetTex();
|
2025-06-22 09:24:39 +02:00
|
|
|
g_pRenderer->renderTextureMix(TEXBOX, TEX, PENDINGTEX, 1.0, crossFadeProgress->value(), 0);
|
|
|
|
|
} else
|
2025-08-27 09:32:57 +00:00
|
|
|
g_pRenderer->renderTexture(TEXBOX, TEX, 1, 0);
|
2025-06-22 09:24:39 +02:00
|
|
|
|
|
|
|
|
return crossFadeProgress->isBeingAnimated() || data.opacity < 1.0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-08 10:45:09 +02:00
|
|
|
void CBackground::onAssetUpdate(ResourceID id, ASP<CTexture> newAsset) {
|
|
|
|
|
pendingResource = false;
|
|
|
|
|
|
|
|
|
|
if (!newAsset)
|
|
|
|
|
Debug::log(ERR, "Background asset update failed, resourceID: {} not available on update!", id);
|
|
|
|
|
else if (newAsset->m_iType == TEXTURE_INVALID) {
|
|
|
|
|
g_asyncResourceManager->unload(newAsset);
|
|
|
|
|
Debug::log(ERR, "New background asset has an invalid texture!");
|
|
|
|
|
} else {
|
|
|
|
|
pendingAsset = newAsset;
|
|
|
|
|
crossFadeProgress->setValueAndWarp(0);
|
|
|
|
|
*crossFadeProgress = 1.0;
|
|
|
|
|
|
|
|
|
|
crossFadeProgress->setCallbackOnEnd(
|
|
|
|
|
[REF = m_self, id](auto) {
|
|
|
|
|
if (const auto PSELF = REF.lock()) {
|
|
|
|
|
if (PSELF->asset)
|
|
|
|
|
g_asyncResourceManager->unload(PSELF->asset);
|
|
|
|
|
PSELF->asset = PSELF->pendingAsset;
|
|
|
|
|
PSELF->pendingAsset = nullptr;
|
|
|
|
|
PSELF->resourceID = id;
|
|
|
|
|
|
|
|
|
|
PSELF->blurredFB->destroyBuffer();
|
|
|
|
|
PSELF->blurredFB = std::move(PSELF->pendingBlurredFB);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-22 09:24:39 +02:00
|
|
|
void CBackground::plantReloadTimer() {
|
|
|
|
|
|
|
|
|
|
if (reloadTime == 0)
|
|
|
|
|
reloadTimer = g_pHyprlock->addTimer(std::chrono::hours(1), [REF = m_self](auto, auto) { onReloadTimer(REF); }, nullptr, true);
|
|
|
|
|
else if (reloadTime > 0)
|
|
|
|
|
reloadTimer = g_pHyprlock->addTimer(std::chrono::seconds(reloadTime), [REF = m_self](auto, auto) { onReloadTimer(REF); }, nullptr, true);
|
2024-12-18 16:28:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CBackground::onReloadTimerUpdate() {
|
|
|
|
|
const std::string OLDPATH = path;
|
|
|
|
|
|
|
|
|
|
// Path parsing and early returns
|
|
|
|
|
|
|
|
|
|
if (!reloadCommand.empty()) {
|
2026-02-20 19:49:58 +03:00
|
|
|
path = runAndGetPath(reloadCommand);
|
2024-12-18 16:28:05 +01:00
|
|
|
|
|
|
|
|
if (path.empty())
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2024-12-26 15:43:11 +00:00
|
|
|
const auto MTIME = std::filesystem::last_write_time(absolutePath(path, ""));
|
2024-12-18 16:28:05 +01:00
|
|
|
if (OLDPATH == path && MTIME == modificationTime)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
modificationTime = MTIME;
|
2025-10-08 10:45:09 +02:00
|
|
|
if (OLDPATH == path)
|
|
|
|
|
m_imageRevision++;
|
|
|
|
|
else
|
|
|
|
|
m_imageRevision = 0;
|
2024-12-18 16:28:05 +01:00
|
|
|
} catch (std::exception& e) {
|
|
|
|
|
path = OLDPATH;
|
|
|
|
|
Debug::log(ERR, "{}", e.what());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-08 10:45:09 +02:00
|
|
|
if (pendingResource)
|
2024-12-18 16:28:05 +01:00
|
|
|
return;
|
|
|
|
|
|
2025-10-08 10:45:09 +02:00
|
|
|
pendingResource = true;
|
2024-12-18 16:28:05 +01:00
|
|
|
|
2025-10-08 10:45:09 +02:00
|
|
|
// Issue the next request
|
|
|
|
|
AWP<IWidget> widget(m_self);
|
|
|
|
|
g_asyncResourceManager->requestImage(path, m_imageRevision, widget);
|
2025-02-06 16:36:08 +05:00
|
|
|
}
|