hyprlock/src/renderer/widgets/Background.cpp

406 lines
14 KiB
C++
Raw Normal View History

2024-02-18 23:08:03 +00:00
#include "Background.hpp"
#include "../Renderer.hpp"
#include "../AsyncResourceManager.hpp"
#include "../Framebuffer.hpp"
#include "../../core/hyprlock.hpp"
#include "../../helpers/Log.hpp"
#include "../../helpers/MiscFunctions.hpp"
#include "../../core/AnimationManager.hpp"
#include "../../config/ConfigManager.hpp"
#include <hyprlang.hpp>
#include <filesystem>
#include <GLES3/gl32.h>
CBackground::CBackground() {
blurredFB = makeUnique<CFramebuffer>();
pendingBlurredFB = makeUnique<CFramebuffer>();
transformedScFB = makeUnique<CFramebuffer>();
}
CBackground::~CBackground() {
reset();
}
void CBackground::registerSelf(const ASP<CBackground>& self) {
m_self = self;
}
2024-02-18 23:08:03 +00: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;
}
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
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()); //
}
isScreenshot = path == "screenshot";
viewport = pOutput->getViewport();
outputPort = pOutput->stringPort;
transform = wlTransformToHyprutils(invertTransform(pOutput->transform));
scResourceID = CAsyncResourceManager::resourceIDForScreencopy(pOutput->stringPort);
g_pAnimationManager->createAnimation(0.f, crossFadeProgress, g_pConfigManager->m_AnimationTree.getConfig("fadeIn"));
if (!g_asyncResourceManager->checkIdPresent(scResourceID)) {
Debug::log(LOG, "Missing screenshot for output {}", outputPort);
scResourceID = 0;
}
if (!reloadCommand.empty() && path.empty())
path = runAndGetPath(reloadCommand);
if (isScreenshot) {
resourceID = scResourceID; // Fallback to solid background:color when scResourceID==0
if (!g_pHyprlock->getScreencopy()) {
Debug::log(ERR, "No screencopy support! path=screenshot won't work. Falling back to background color.");
resourceID = 0;
}
} else if (!path.empty()) {
if (CVideoBackend::isVideoFile(path)) {
m_videoBackend = makeUnique<CVideoBackend>();
if (!m_videoBackend->open(path)) {
Debug::log(ERR, "CBackground: failed to open '{}' as video, falling back to image", path);
m_videoBackend.reset();
resourceID = g_asyncResourceManager->requestImage(path, m_imageRevision, nullptr);
} else {
m_uploadBuffer.resize(4 * m_videoBackend->frameW() * m_videoBackend->frameH());
}
} else {
resourceID = g_asyncResourceManager->requestImage(path, m_imageRevision, nullptr);
}
}
if (!reloadCommand.empty() && reloadTime > -1 && !m_videoBackend) {
try {
if (!isScreenshot)
modificationTime = std::filesystem::last_write_time(absolutePath(path, ""));
} catch (std::exception& e) { Debug::log(ERR, "{}", e.what()); }
plantReloadTimer(); // No reloads if reloadCommand is empty
}
2024-02-18 23:08:03 +00:00
}
void CBackground::reset() {
if (m_videoBackend) {
m_videoBackend.reset();
m_videoTexture.destroyTexture();
m_uploadBuffer.clear();
}
if (reloadTimer) {
reloadTimer->cancel();
reloadTimer.reset();
}
blurredFB->destroyBuffer();
pendingBlurredFB->destroyBuffer();
}
void CBackground::updatePrimaryAsset() {
if (asset || resourceID == 0)
return;
asset = g_asyncResourceManager->getAssetByID(resourceID);
if (!asset)
return;
const bool NEEDFB = (isScreenshot || blurPasses > 0 || asset->m_vSize != viewport || transform != HYPRUTILS_TRANSFORM_NORMAL) && (!blurredFB->isAllocated() || firstRender);
if (NEEDFB)
renderToFB(*asset, *blurredFB, blurPasses, isScreenshot);
}
void CBackground::updatePendingAsset() {
// For crossfading a new asset
if (!pendingAsset || blurPasses == 0 || pendingBlurredFB->isAllocated())
return;
renderToFB(*pendingAsset, *pendingBlurredFB, blurPasses);
}
void CBackground::updateScAsset() {
if (scAsset || scResourceID == 0)
return;
// path=screenshot -> scAsset = asset
scAsset = (asset && isScreenshot) ? asset : g_asyncResourceManager->getAssetByID(scResourceID);
if (!scAsset)
return;
const bool NEEDSCTRANSFORM = transform != HYPRUTILS_TRANSFORM_NORMAL;
if (NEEDSCTRANSFORM)
renderToFB(*scAsset, *transformedScFB, 0, true);
}
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;
return (blurredFB->isAllocated()) ? blurredFB->m_cTex : *asset;
}
const CTexture& CBackground::getPendingAssetTex() const {
return (pendingBlurredFB->isAllocated()) ? pendingBlurredFB->m_cTex : *pendingAsset;
}
const CTexture& CBackground::getScAssetTex() const {
return (transformedScFB->isAllocated()) ? transformedScFB->m_cTex : *scAsset;
}
void CBackground::renderRect(CHyprColor color) {
CBox monbox = {0, 0, viewport.x, viewport.y};
g_pRenderer->renderRect(monbox, color, 0);
}
static void onReloadTimer(AWP<CBackground> ref) {
if (auto PBG = ref.lock(); PBG) {
PBG->onReloadTimerUpdate();
PBG->plantReloadTimer();
}
}
static CBox getScaledBoxForTextureSize(const Vector2D& size, const Vector2D& viewport) {
CBox texbox = {{}, size};
2024-02-18 23:08:03 +00:00
float scaleX = viewport.x / size.x;
float scaleY = viewport.y / size.y;
texbox.w *= std::max(scaleX, scaleY);
texbox.h *= std::max(scaleX, scaleY);
2024-02-18 23:08:03 +00:00
if (scaleX > scaleY)
texbox.y = -(texbox.h - viewport.y) / 2.f;
else
texbox.x = -(texbox.w - viewport.x) / 2.f;
texbox.round();
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);
if (!fb.isAllocated())
fb.alloc(viewport.x, viewport.y); // TODO 10 bit
fb.bind();
g_pRenderer->renderTexture(TEXBOX, tex, 1.0, 0, applyTransform ? transform : HYPRUTILS_TRANSFORM_NORMAL);
2024-02-21 22:19:01 +00:00
if (blurPasses > 0)
g_pRenderer->blurFB(fb,
CRenderer::SBlurParams{
.size = blurSize,
.passes = passes,
.noise = noise,
.contrast = contrast,
.brightness = brightness,
.vibrancy = vibrancy,
.vibrancy_darkness = vibrancy_darkness,
});
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
}
2024-02-21 22:19:01 +00:00
bool CBackground::draw(const SRenderData& data) {
// ── Video background fast path ────────────────────────────────────────
if (m_videoBackend) {
if (m_videoBackend->swapFrame(m_uploadBuffer)) {
const int W = m_videoBackend->frameW();
const int H = m_videoBackend->frameH();
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, W, H, 0,
GL_RGBA, GL_UNSIGNED_BYTE, m_uploadBuffer.data());
glBindTexture(GL_TEXTURE_2D, 0);
m_videoTexture.m_vSize = {(double)W, (double)H};
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, W, H,
GL_RGBA, GL_UNSIGNED_BYTE, m_uploadBuffer.data());
glBindTexture(GL_TEXTURE_2D, 0);
}
if (blurPasses > 0)
renderToFB(m_videoTexture, *blurredFB, blurPasses);
}
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 ────────────────────────────────────────────────────
updatePrimaryAsset();
updatePendingAsset();
updateScAsset();
if (asset && asset->m_iType == TEXTURE_INVALID) {
g_asyncResourceManager->unload(asset);
resourceID = 0;
renderRect(color);
return false;
}
if (!asset || resourceID == 0) {
// fade in/out with a solid color
if (data.opacity < 1.0 && scAsset) {
const auto& SCTEX = getScAssetTex();
const auto SCTEXBOX = getScaledBoxForTextureSize(SCTEX.m_vSize, viewport);
g_pRenderer->renderTexture(SCTEXBOX, SCTEX, 1, 0, HYPRUTILS_TRANSFORM_FLIPPED_180);
CHyprColor col = color;
col.a *= data.opacity;
renderRect(col);
return true;
}
renderRect(color);
return !asset && resourceID > 0; // resource not ready
}
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();
g_pRenderer->renderTextureMix(TEXBOX, TEX, PENDINGTEX, 1.0, crossFadeProgress->value(), 0);
} else
g_pRenderer->renderTexture(TEXBOX, TEX, 1, 0);
return crossFadeProgress->isBeingAnimated() || data.opacity < 1.0;
}
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);
}
}
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);
}
void CBackground::onReloadTimerUpdate() {
const std::string OLDPATH = path;
// Path parsing and early returns
if (!reloadCommand.empty()) {
path = runAndGetPath(reloadCommand);
if (path.empty())
return;
}
try {
const auto MTIME = std::filesystem::last_write_time(absolutePath(path, ""));
if (OLDPATH == path && MTIME == modificationTime)
return;
modificationTime = MTIME;
if (OLDPATH == path)
m_imageRevision++;
else
m_imageRevision = 0;
} catch (std::exception& e) {
path = OLDPATH;
Debug::log(ERR, "{}", e.what());
return;
}
if (pendingResource)
return;
pendingResource = true;
// Issue the next request
AWP<IWidget> widget(m_self);
g_asyncResourceManager->requestImage(path, m_imageRevision, widget);
}