diff --git a/src/Compositor.cpp b/src/Compositor.cpp index a5826d195..d9de51959 100644 --- a/src/Compositor.cpp +++ b/src/Compositor.cpp @@ -74,7 +74,7 @@ #include "plugins/PluginSystem.hpp" #include "hyprerror/HyprError.hpp" #include "notification/NotificationOverlay.hpp" -#include "debug/HyprDebugOverlay.hpp" +#include "debug/Overlay.hpp" #include "helpers/MonitorFrameScheduler.hpp" #include "i18n/Engine.hpp" #include "layout/LayoutManager.hpp" @@ -591,7 +591,7 @@ void CCompositor::cleanup() { g_pCursorManager.reset(); g_pPluginSystem.reset(); Notification::overlay().reset(); - g_pDebugOverlay.reset(); + Debug::overlay().reset(); g_pEventManager.reset(); g_pSessionLockManager.reset(); g_pHyprRenderer.reset(); @@ -694,8 +694,8 @@ void CCompositor::initManagers(eManagersInitStage stage) { Log::logger->log(Log::DEBUG, "Creating the SessionLockManager!"); g_pSessionLockManager = makeUnique(); - Log::logger->log(Log::DEBUG, "Creating the HyprDebugOverlay!"); - g_pDebugOverlay = makeUnique(); + Log::logger->log(Log::DEBUG, "Creating the Debug Overlay!"); + Debug::overlay(); Log::logger->log(Log::DEBUG, "Creating the NotificationOverlay!"); Notification::overlay(); diff --git a/src/debug/HyprDebugOverlay.cpp b/src/debug/HyprDebugOverlay.cpp deleted file mode 100644 index 17ce12fab..000000000 --- a/src/debug/HyprDebugOverlay.cpp +++ /dev/null @@ -1,268 +0,0 @@ -#include -#include "HyprDebugOverlay.hpp" -#include "config/ConfigValue.hpp" -#include "../Compositor.hpp" -#include "../render/pass/TexPassElement.hpp" -#include "../render/Renderer.hpp" -#include "../managers/animation/AnimationManager.hpp" -#include "../desktop/state/FocusState.hpp" - -CHyprDebugOverlay::CHyprDebugOverlay() { - m_texture = g_pHyprRenderer->createTexture(); -} - -void CHyprMonitorDebugOverlay::renderData(PHLMONITOR pMonitor, float durationUs) { - static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); - - if (!*PDEBUGOVERLAY) - return; - - m_lastRenderTimes.emplace_back(durationUs / 1000.f); - - if (m_lastRenderTimes.size() > sc(pMonitor->m_refreshRate)) - m_lastRenderTimes.pop_front(); - - if (!m_monitor) - m_monitor = pMonitor; -} - -void CHyprMonitorDebugOverlay::renderDataNoOverlay(PHLMONITOR pMonitor, float durationUs) { - static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); - - if (!*PDEBUGOVERLAY) - return; - - m_lastRenderTimesNoOverlay.emplace_back(durationUs / 1000.f); - - if (m_lastRenderTimesNoOverlay.size() > sc(pMonitor->m_refreshRate)) - m_lastRenderTimesNoOverlay.pop_front(); - - if (!m_monitor) - m_monitor = pMonitor; -} - -void CHyprMonitorDebugOverlay::frameData(PHLMONITOR pMonitor) { - static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); - - if (!*PDEBUGOVERLAY) - return; - - m_lastFrametimes.emplace_back(std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - m_lastFrame).count() / 1000.f); - - if (m_lastFrametimes.size() > sc(pMonitor->m_refreshRate)) - m_lastFrametimes.pop_front(); - - m_lastFrame = std::chrono::high_resolution_clock::now(); - - if (!m_monitor) - m_monitor = pMonitor; - - // anim data too - const auto PMONITORFORTICKS = g_pHyprRenderer->m_mostHzMonitor ? g_pHyprRenderer->m_mostHzMonitor.lock() : Desktop::focusState()->monitor(); - if (PMONITORFORTICKS == pMonitor) { - if (m_lastAnimationTicks.size() > sc(PMONITORFORTICKS->m_refreshRate)) - m_lastAnimationTicks.pop_front(); - - m_lastAnimationTicks.push_back(g_pAnimationManager->m_lastTickTimeMs); - } -} - -int CHyprMonitorDebugOverlay::draw(int offset) { - - if (!m_monitor) - return 0; - - // get avg fps - float avgFrametime = 0; - float maxFrametime = 0; - float minFrametime = 9999; - for (auto const& ft : m_lastFrametimes) { - if (ft > maxFrametime) - maxFrametime = ft; - if (ft < minFrametime) - minFrametime = ft; - avgFrametime += ft; - } - float varFrametime = maxFrametime - minFrametime; - avgFrametime /= m_lastFrametimes.empty() ? 1 : m_lastFrametimes.size(); - - float avgRenderTime = 0; - float maxRenderTime = 0; - float minRenderTime = 9999; - for (auto const& rt : m_lastRenderTimes) { - if (rt > maxRenderTime) - maxRenderTime = rt; - if (rt < minRenderTime) - minRenderTime = rt; - avgRenderTime += rt; - } - float varRenderTime = maxRenderTime - minRenderTime; - avgRenderTime /= m_lastRenderTimes.empty() ? 1 : m_lastRenderTimes.size(); - - float avgRenderTimeNoOverlay = 0; - float maxRenderTimeNoOverlay = 0; - float minRenderTimeNoOverlay = 9999; - for (auto const& rt : m_lastRenderTimesNoOverlay) { - if (rt > maxRenderTimeNoOverlay) - maxRenderTimeNoOverlay = rt; - if (rt < minRenderTimeNoOverlay) - minRenderTimeNoOverlay = rt; - avgRenderTimeNoOverlay += rt; - } - float varRenderTimeNoOverlay = maxRenderTimeNoOverlay - minRenderTimeNoOverlay; - avgRenderTimeNoOverlay /= m_lastRenderTimes.empty() ? 1 : m_lastRenderTimes.size(); - - float avgAnimMgrTick = 0; - float maxAnimMgrTick = 0; - float minAnimMgrTick = 9999; - for (auto const& at : m_lastAnimationTicks) { - if (at > maxAnimMgrTick) - maxAnimMgrTick = at; - if (at < minAnimMgrTick) - minAnimMgrTick = at; - avgAnimMgrTick += at; - } - float varAnimMgrTick = maxAnimMgrTick - minAnimMgrTick; - avgAnimMgrTick /= m_lastAnimationTicks.empty() ? 1 : m_lastAnimationTicks.size(); - - const float FPS = 1.f / (avgFrametime / 1000.f); // frametimes are in ms - const float idealFPS = m_lastFrametimes.size(); - - static auto fontFamily = CConfigValue("misc:font_family"); - PangoLayout* layoutText = pango_cairo_create_layout(g_pDebugOverlay->m_cairo); - PangoFontDescription* pangoFD = pango_font_description_new(); - - pango_font_description_set_family(pangoFD, (*fontFamily).c_str()); - pango_font_description_set_style(pangoFD, PANGO_STYLE_NORMAL); - pango_font_description_set_weight(pangoFD, PANGO_WEIGHT_NORMAL); - - float maxTextW = 0; - int fontSize = 0; - auto cr = g_pDebugOverlay->m_cairo; - - auto showText = [cr, layoutText, pangoFD, &maxTextW, &fontSize](const char* text, int size) { - if (fontSize != size) { - pango_font_description_set_absolute_size(pangoFD, size * PANGO_SCALE); - pango_layout_set_font_description(layoutText, pangoFD); - fontSize = size; - } - - pango_layout_set_text(layoutText, text, -1); - pango_cairo_show_layout(cr, layoutText); - - int textW = 0, textH = 0; - pango_layout_get_size(layoutText, &textW, &textH); - textW /= PANGO_SCALE; - textH /= PANGO_SCALE; - if (textW > maxTextW) - maxTextW = textW; - - // move to next line - cairo_rel_move_to(cr, 0, fontSize + 1); - }; - - const int MARGIN_TOP = 8; - const int MARGIN_LEFT = 4; - cairo_move_to(cr, MARGIN_LEFT, MARGIN_TOP + offset); - cairo_set_source_rgba(g_pDebugOverlay->m_cairo, 1.f, 1.f, 1.f, 1.f); - - std::string text; - showText(m_monitor->m_name.c_str(), 10); - - if (FPS > idealFPS * 0.95f) - cairo_set_source_rgba(g_pDebugOverlay->m_cairo, 0.2f, 1.f, 0.2f, 1.f); - else if (FPS > idealFPS * 0.8f) - cairo_set_source_rgba(g_pDebugOverlay->m_cairo, 1.f, 1.f, 0.2f, 1.f); - else - cairo_set_source_rgba(g_pDebugOverlay->m_cairo, 1.f, 0.2f, 0.2f, 1.f); - - text = std::format("{} FPS", sc(FPS)); - showText(text.c_str(), 16); - - cairo_set_source_rgba(g_pDebugOverlay->m_cairo, 1.f, 1.f, 1.f, 1.f); - - text = std::format("Avg Frametime: {:.2f}ms (var {:.2f}ms)", avgFrametime, varFrametime); - showText(text.c_str(), 10); - - text = std::format("Avg Rendertime: {:.2f}ms (var {:.2f}ms)", avgRenderTime, varRenderTime); - showText(text.c_str(), 10); - - text = std::format("Avg Rendertime (No Overlay): {:.2f}ms (var {:.2f}ms)", avgRenderTimeNoOverlay, varRenderTimeNoOverlay); - showText(text.c_str(), 10); - - text = std::format("Avg Anim Tick: {:.2f}ms (var {:.2f}ms) ({:.2f} TPS)", avgAnimMgrTick, varAnimMgrTick, 1.0 / (avgAnimMgrTick / 1000.0)); - showText(text.c_str(), 10); - - pango_font_description_free(pangoFD); - g_object_unref(layoutText); - - double posX = 0, posY = 0; - cairo_get_current_point(cr, &posX, &posY); - - g_pHyprRenderer->damageBox(m_lastDrawnBox); - m_lastDrawnBox = {sc(g_pCompositor->m_monitors.front()->m_position.x) + MARGIN_LEFT - 1, - sc(g_pCompositor->m_monitors.front()->m_position.y) + offset + MARGIN_TOP - 1, sc(maxTextW) + 2, posY - offset - MARGIN_TOP + 2}; - g_pHyprRenderer->damageBox(m_lastDrawnBox); - - return posY - offset; -} - -void CHyprDebugOverlay::renderData(PHLMONITOR pMonitor, float durationUs) { - static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); - - if (!*PDEBUGOVERLAY) - return; - - m_monitorOverlays[pMonitor].renderData(pMonitor, durationUs); -} - -void CHyprDebugOverlay::renderDataNoOverlay(PHLMONITOR pMonitor, float durationUs) { - static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); - - if (!*PDEBUGOVERLAY) - return; - - m_monitorOverlays[pMonitor].renderDataNoOverlay(pMonitor, durationUs); -} - -void CHyprDebugOverlay::frameData(PHLMONITOR pMonitor) { - static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); - - if (!*PDEBUGOVERLAY) - return; - - m_monitorOverlays[pMonitor].frameData(pMonitor); -} - -void CHyprDebugOverlay::draw() { - - const auto PMONITOR = g_pCompositor->m_monitors.front(); - - if (!m_cairoSurface || !m_cairo) { - m_cairoSurface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, PMONITOR->m_pixelSize.x, PMONITOR->m_pixelSize.y); - m_cairo = cairo_create(m_cairoSurface); - } - - // clear the pixmap - cairo_save(m_cairo); - cairo_set_operator(m_cairo, CAIRO_OPERATOR_CLEAR); - cairo_paint(m_cairo); - cairo_restore(m_cairo); - - // draw the things - int offsetY = 0; - for (auto const& m : g_pCompositor->m_monitors) { - offsetY += m_monitorOverlays[m].draw(offsetY); - offsetY += 5; // for padding between mons - } - - cairo_surface_flush(m_cairoSurface); - - // copy the data to an OpenGL texture we have - m_texture = g_pHyprRenderer->createTexture(m_cairoSurface); - - CTexPassElement::SRenderData data; - data.tex = m_texture; - data.box = {0, 0, PMONITOR->m_pixelSize.x, PMONITOR->m_pixelSize.y}; - g_pHyprRenderer->m_renderPass.add(makeUnique(std::move(data))); -} diff --git a/src/debug/HyprDebugOverlay.hpp b/src/debug/HyprDebugOverlay.hpp deleted file mode 100644 index 375ecc2c0..000000000 --- a/src/debug/HyprDebugOverlay.hpp +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once - -#include "../defines.hpp" -#include "../render/Texture.hpp" -#include -#include -#include - -namespace Render { - class IHyprRenderer; -} - -class CHyprMonitorDebugOverlay { - public: - int draw(int offset); - - void renderData(PHLMONITOR pMonitor, float durationUs); - void renderDataNoOverlay(PHLMONITOR pMonitor, float durationUs); - void frameData(PHLMONITOR pMonitor); - - private: - std::deque m_lastFrametimes; - std::deque m_lastRenderTimes; - std::deque m_lastRenderTimesNoOverlay; - std::deque m_lastAnimationTicks; - std::chrono::high_resolution_clock::time_point m_lastFrame; - PHLMONITORREF m_monitor; - CBox m_lastDrawnBox; - - friend class Render::IHyprRenderer; -}; - -class CHyprDebugOverlay { - public: - CHyprDebugOverlay(); - void draw(); - void renderData(PHLMONITOR, float durationUs); - void renderDataNoOverlay(PHLMONITOR, float durationUs); - void frameData(PHLMONITOR); - - private: - std::map m_monitorOverlays; - - cairo_surface_t* m_cairoSurface = nullptr; - cairo_t* m_cairo = nullptr; - - SP m_texture; - - friend class CHyprMonitorDebugOverlay; - friend class Render::IHyprRenderer; -}; - -inline UP g_pDebugOverlay; diff --git a/src/debug/Overlay.cpp b/src/debug/Overlay.cpp new file mode 100644 index 000000000..694bb3207 --- /dev/null +++ b/src/debug/Overlay.cpp @@ -0,0 +1,503 @@ +#include "Overlay.hpp" +#include "config/ConfigValue.hpp" +#include "../Compositor.hpp" +#include "../render/pass/RectPassElement.hpp" +#include "../render/pass/TexPassElement.hpp" +#include "../render/Renderer.hpp" +#include "../managers/animation/AnimationManager.hpp" +#include "../desktop/state/FocusState.hpp" +#include +#include +#include + +namespace { + constexpr float OVERLAY_REFRESH_INTERVAL_MS = 200.F; + constexpr int OVERLAY_MARGIN_TOP = 4; + constexpr int OVERLAY_MARGIN_LEFT = 4; + constexpr int OVERLAY_LINE_GAP = 1; + constexpr int OVERLAY_MONITOR_GAP = 5; + constexpr int OVERLAY_BOX_MARGIN = 5; + + constexpr int OVERLAY_FPS_GRAPH_HISTORY_SEC = 30; + constexpr int OVERLAY_FPS_GRAPH_BAR_WIDTH = 3; + constexpr int OVERLAY_FPS_GRAPH_BAR_GAP = 1; + constexpr int OVERLAY_FPS_GRAPH_HEIGHT = 22; + constexpr int OVERLAY_FPS_GRAPH_PADDING = 2; + constexpr int OVERLAY_FPS_GRAPH_GAP_TOP = 2; + constexpr float OVERLAY_FPS_GRAPH_BG_ALPHA = 0.35F; + + const CHyprColor FPS_COLOR_BAD = CHyprColor{1.F, 0.2F, 0.2F, 1.F}; + const CHyprColor FPS_COLOR_GOOD = CHyprColor{0.2F, 1.F, 0.2F, 1.F}; + + struct SFPSGraphLayout { + float innerWidth = 0.F; + float innerHeight = 0.F; + float width = 0.F; + float height = 0.F; + }; + + struct SFPSGraphDrawResult { + float width = 0.F; + float bottomY = 0.F; + }; +} + +static Hyprgraphics::CColor::SOkLab lerp(const Hyprgraphics::CColor::SOkLab& a, const Hyprgraphics::CColor::SOkLab& b, float ratio) { + return Hyprgraphics::CColor::SOkLab{ + .l = std::lerp(a.l, b.l, ratio), + .a = std::lerp(a.a, b.a, ratio), + .b = std::lerp(a.b, b.b, ratio), + }; +} + +static CHyprColor fpsBarColor(float normalizedFPS) { + return CHyprColor{Hyprgraphics::CColor{lerp(FPS_COLOR_BAD.asOkLab(), FPS_COLOR_GOOD.asOkLab(), normalizedFPS)}, 1.F}; +} + +static SFPSGraphLayout fpsGraphLayout() { + const float INNERWIDTH = sc(OVERLAY_FPS_GRAPH_HISTORY_SEC * OVERLAY_FPS_GRAPH_BAR_WIDTH + (OVERLAY_FPS_GRAPH_HISTORY_SEC - 1) * OVERLAY_FPS_GRAPH_BAR_GAP); + const float INNERHEIGHT = sc(OVERLAY_FPS_GRAPH_HEIGHT); + + return { + .innerWidth = INNERWIDTH, + .innerHeight = INNERHEIGHT, + .width = INNERWIDTH + OVERLAY_FPS_GRAPH_PADDING * 2.F, + .height = INNERHEIGHT + OVERLAY_FPS_GRAPH_PADDING * 2.F, + }; +} + +static SFPSGraphDrawResult drawFPSGraph(float x, float y, float idealFPS, const std::deque& fpsHistory) { + const auto LAYOUT = fpsGraphLayout(); + + CRectPassElement::SRectData bgData; + bgData.box = {x, y, LAYOUT.width, LAYOUT.height}; + bgData.color = CHyprColor{0.F, 0.F, 0.F, OVERLAY_FPS_GRAPH_BG_ALPHA}; + bgData.round = 2; + g_pHyprRenderer->m_renderPass.add(makeUnique(bgData)); + + const size_t BARCOUNT = std::min(fpsHistory.size(), sc(OVERLAY_FPS_GRAPH_HISTORY_SEC)); + const size_t LEADINGBLANKBARS = sc(OVERLAY_FPS_GRAPH_HISTORY_SEC) - BARCOUNT; + + for (size_t bar = 0; bar < BARCOUNT; ++bar) { + const float FPSVALUE = fpsHistory[fpsHistory.size() - BARCOUNT + bar]; + const float NORMALIZEDFPS = std::clamp(FPSVALUE / idealFPS, 0.F, 1.F); + const float BARHEIGHT = std::max(1.F, std::round(NORMALIZEDFPS * LAYOUT.innerHeight)); + const float BARX = x + OVERLAY_FPS_GRAPH_PADDING + sc((LEADINGBLANKBARS + bar) * (OVERLAY_FPS_GRAPH_BAR_WIDTH + OVERLAY_FPS_GRAPH_BAR_GAP)); + const float BARY = y + OVERLAY_FPS_GRAPH_PADDING + (LAYOUT.innerHeight - BARHEIGHT); + + CRectPassElement::SRectData barData; + barData.box = {BARX, BARY, sc(OVERLAY_FPS_GRAPH_BAR_WIDTH), BARHEIGHT}; + barData.color = fpsBarColor(NORMALIZEDFPS); + g_pHyprRenderer->m_renderPass.add(makeUnique(barData)); + } + + return { + .width = LAYOUT.width, + .bottomY = y + LAYOUT.height, + }; +} + +using namespace Debug; + +UP& Debug::overlay() { + static UP p = makeUnique(); + return p; +} + +COverlay::COverlay() { + m_frameTimer.reset(); +} + +void CMonitorOverlay::renderData(PHLMONITOR pMonitor, float durationUs) { + static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); + + if (!*PDEBUGOVERLAY) + return; + + m_monitor = pMonitor; + + m_lastRenderTimes.emplace_back(durationUs / 1000.F); + + const auto SAMPLELIMIT = std::max(1, sc(std::ceil(std::max(1.F, pMonitor->m_refreshRate)))); + + if (m_lastRenderTimes.size() > SAMPLELIMIT) + m_lastRenderTimes.pop_front(); +} + +void CMonitorOverlay::renderDataNoOverlay(PHLMONITOR pMonitor, float durationUs) { + static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); + + if (!*PDEBUGOVERLAY) + return; + + m_monitor = pMonitor; + + m_lastRenderTimesNoOverlay.emplace_back(durationUs / 1000.F); + + const auto SAMPLELIMIT = std::max(1, sc(std::ceil(std::max(1.F, pMonitor->m_refreshRate)))); + + if (m_lastRenderTimesNoOverlay.size() > SAMPLELIMIT) + m_lastRenderTimesNoOverlay.pop_front(); +} + +void CMonitorOverlay::frameData(PHLMONITOR pMonitor) { + static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); + + if (!*PDEBUGOVERLAY) + return; + + m_monitor = pMonitor; + + const auto NOW = std::chrono::high_resolution_clock::now(); + if (m_lastFrame.time_since_epoch().count() != 0) + m_lastFrametimes.emplace_back(std::chrono::duration_cast(NOW - m_lastFrame).count() / 1000.F); + + const auto SAMPLELIMIT = std::max(1, sc(std::ceil(std::max(1.F, pMonitor->m_refreshRate)))); + + if (m_lastFrametimes.size() > SAMPLELIMIT) + m_lastFrametimes.pop_front(); + + m_lastFrame = NOW; + + if (m_fpsSecondStart.time_since_epoch().count() == 0) + m_fpsSecondStart = NOW; + + ++m_framesInCurrentSecond; + + const auto SECONDWINDOWMS = std::chrono::duration_cast(NOW - m_fpsSecondStart).count(); + if (SECONDWINDOWMS >= 1000) { + const float ELAPSEDSECONDS = SECONDWINDOWMS / 1000.F; + const float IDEALFPS = std::max(1.F, pMonitor->m_refreshRate); + const float FPSINWINDOW = ELAPSEDSECONDS > 0.F ? m_framesInCurrentSecond / ELAPSEDSECONDS : 0.F; + + m_lastFPSPerSecond.emplace_back(std::clamp(FPSINWINDOW, 0.F, IDEALFPS)); + + if (m_lastFPSPerSecond.size() > OVERLAY_FPS_GRAPH_HISTORY_SEC) + m_lastFPSPerSecond.pop_front(); + + m_framesInCurrentSecond = 0; + m_fpsSecondStart = NOW; + } + + // anim data too + const auto PMONITORFORTICKS = g_pHyprRenderer->m_mostHzMonitor ? g_pHyprRenderer->m_mostHzMonitor.lock() : Desktop::focusState()->monitor(); + if (PMONITORFORTICKS && PMONITORFORTICKS == pMonitor) { + const auto TICKLIMIT = std::max(1, sc(std::ceil(std::max(1.F, PMONITORFORTICKS->m_refreshRate)))); + + if (m_lastAnimationTicks.size() > TICKLIMIT) + m_lastAnimationTicks.pop_front(); + + m_lastAnimationTicks.push_back(g_pAnimationManager->m_lastTickTimeMs); + } +} + +const CBox& CMonitorOverlay::lastDrawnBox() const { + return m_lastDrawnBox; +} + +void CMonitorOverlay::updateLine(size_t idx, const std::string& text, const CHyprColor& color, int fontSize, const std::string& fontFamily) { + if (m_cachedLines.size() <= idx) + m_cachedLines.resize(idx + 1); + + auto& line = m_cachedLines[idx]; + if (line.texture && line.text == text && line.fontSize == fontSize && line.color == color) + return; + + line.text = text; + line.color = color; + line.fontSize = fontSize; + line.texture = g_pHyprRenderer->renderText(text, color, fontSize, false, fontFamily); +} + +void CMonitorOverlay::rebuildCache() { + m_cachedLines.clear(); + + if (!m_monitor) + return; + + const auto PMONITOR = m_monitor.lock(); + if (!PMONITOR) + return; + + auto metricsFromSamples = [](const std::deque& samples) -> SMetricData { + SMetricData metric; + + if (samples.empty()) + return metric; + + metric.min = std::numeric_limits::max(); + metric.max = std::numeric_limits::lowest(); + + for (const auto sample : samples) { + metric.avg += sample; + metric.min = std::min(metric.min, sample); + metric.max = std::max(metric.max, sample); + } + + metric.avg /= samples.size(); + metric.var = metric.max - metric.min; + + return metric; + }; + + const auto FRAMEMETRICS = metricsFromSamples(m_lastFrametimes); + const auto RENDERMETRICS = metricsFromSamples(m_lastRenderTimes); + const auto RENDERMETRICSNOOVL = metricsFromSamples(m_lastRenderTimesNoOverlay); + const auto ANIMATIONTICKMETRICS = metricsFromSamples(m_lastAnimationTicks); + + const float FPS = FRAMEMETRICS.avg <= 0.F ? 0.F : 1000.F / FRAMEMETRICS.avg; + const float IDEALFPS = std::max(1.F, PMONITOR->m_refreshRate); + const float TICKTPS = ANIMATIONTICKMETRICS.avg <= 0.F ? 0.F : 1000.F / ANIMATIONTICKMETRICS.avg; + + static auto FONTFAMILY = CConfigValue("misc:font_family"); + + CHyprColor fpsColor = CHyprColor{1.F, 0.2F, 0.2F, 1.F}; + if (FPS > IDEALFPS * 0.95F) + fpsColor = CHyprColor{0.2F, 1.F, 0.2F, 1.F}; + else if (FPS > IDEALFPS * 0.8F) + fpsColor = CHyprColor{1.F, 1.F, 0.2F, 1.F}; + + size_t idx = 0; + updateLine(idx++, PMONITOR->m_name, CHyprColor{1.F, 1.F, 1.F, 1.F}, 10, *FONTFAMILY); + updateLine(idx++, std::format("{} FPS", sc(std::round(FPS))), fpsColor, 16, *FONTFAMILY); + updateLine(idx++, std::format("Avg Frametime: {:.2f}ms (var {:.2f}ms)", FRAMEMETRICS.avg, FRAMEMETRICS.var), CHyprColor{1.F, 1.F, 1.F, 1.F}, 10, *FONTFAMILY); + updateLine(idx++, std::format("Avg Rendertime: {:.2f}ms (var {:.2f}ms)", RENDERMETRICS.avg, RENDERMETRICS.var), CHyprColor{1.F, 1.F, 1.F, 1.F}, 10, *FONTFAMILY); + updateLine(idx++, std::format("Avg Rendertime (No Overlay): {:.2f}ms (var {:.2f}ms)", RENDERMETRICSNOOVL.avg, RENDERMETRICSNOOVL.var), CHyprColor{1.F, 1.F, 1.F, 1.F}, 10, + *FONTFAMILY); + updateLine(idx++, std::format("Avg Anim Tick: {:.2f}ms (var {:.2f}ms) ({:.2f} TPS)", ANIMATIONTICKMETRICS.avg, ANIMATIONTICKMETRICS.var, TICKTPS), + CHyprColor{1.F, 1.F, 1.F, 1.F}, 10, *FONTFAMILY); + + m_cachedLines.resize(idx); +} + +int CMonitorOverlay::draw(int offset, bool& cacheUpdated) { + cacheUpdated = false; + m_lastDrawnBox = {}; + + if (!m_monitor) + return 0; + + if (!m_cacheValid || m_cacheTimer.getMillis() >= OVERLAY_REFRESH_INTERVAL_MS) { + rebuildCache(); + m_cacheValid = true; + m_cacheTimer.reset(); + cacheUpdated = true; + } + + const auto PMONITOR = m_monitor.lock(); + const float IDEALFPS = PMONITOR ? std::max(1.F, PMONITOR->m_refreshRate) : 1.F; + + float y = offset + OVERLAY_MARGIN_TOP + OVERLAY_BOX_MARGIN; + float maxTextW = 0.F; + + for (size_t i = 0; i < m_cachedLines.size(); ++i) { + const auto& line = m_cachedLines[i]; + if (!line.texture) + continue; + + CTexPassElement::SRenderData data; + data.tex = line.texture; + data.box = {OVERLAY_MARGIN_LEFT + OVERLAY_BOX_MARGIN, y, line.texture->m_size.x, line.texture->m_size.y}; + data.a = 1.F; + g_pHyprRenderer->m_renderPass.add(makeUnique(std::move(data))); + + maxTextW = std::max(maxTextW, sc(line.texture->m_size.x)); + y += line.texture->m_size.y + OVERLAY_LINE_GAP; + + if (i == 1) { + const auto GRAPHDRAW = drawFPSGraph(OVERLAY_MARGIN_LEFT + OVERLAY_BOX_MARGIN, y + OVERLAY_FPS_GRAPH_GAP_TOP, IDEALFPS, m_lastFPSPerSecond); + + maxTextW = std::max(maxTextW, GRAPHDRAW.width); + y = GRAPHDRAW.bottomY + OVERLAY_LINE_GAP; + } + } + + const float HEIGHT = y - offset - OVERLAY_MARGIN_TOP - OVERLAY_BOX_MARGIN; + if (maxTextW <= 0.F || HEIGHT <= 0.F) + return 0; + + m_lastDrawnBox = {OVERLAY_MARGIN_LEFT - 1 + OVERLAY_BOX_MARGIN, offset + OVERLAY_MARGIN_TOP + OVERLAY_BOX_MARGIN - 1, sc(std::ceil(maxTextW)) + 2, + sc(std::ceil(HEIGHT)) + 2}; + return sc(std::ceil(y - offset)); +} + +Vector2D CMonitorOverlay::size() const { + return m_lastDrawnBox.size(); // this shouldn't change much +} + +void COverlay::renderData(PHLMONITOR pMonitor, float durationUs) { + static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); + + if (!*PDEBUGOVERLAY) + return; + + m_monitorOverlays[pMonitor].renderData(pMonitor, durationUs); +} + +void COverlay::renderDataNoOverlay(PHLMONITOR pMonitor, float durationUs) { + static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); + + if (!*PDEBUGOVERLAY) + return; + + m_monitorOverlays[pMonitor].renderDataNoOverlay(pMonitor, durationUs); +} + +void COverlay::frameData(PHLMONITOR pMonitor) { + static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); + + if (!*PDEBUGOVERLAY) + return; + + m_monitorOverlays[pMonitor].frameData(pMonitor); +} + +void COverlay::createWarningTexture(float maxW) { + if (maxW <= 1) { + m_warningTexture.reset(); + m_warningTextureMaxW = 0; + return; + } + + if (maxW == m_warningTextureMaxW) + return; + + static auto FONT = CConfigValue("misc:font_family"); + + m_warningTexture = g_pHyprRenderer->renderText(Hyprgraphics::CTextResource::STextResourceData{ + .text = "[!] FPS might be below your monitor's refresh rate if there are no content updates", + .font = *FONT, + .fontSize = 8, + .color = Colors::YELLOW.asRGB(), + .maxSize = Vector2D{maxW, -1.F}, + }); +} + +void COverlay::draw() { + if (g_pCompositor->m_monitors.empty()) + return; + + const auto PMONITOR = g_pCompositor->m_monitors.front(); + if (!PMONITOR) + return; + + bool haveAnyBox = false; + bool cacheUpdated = false; + int minX = 0; + int minY = 0; + int maxX = 0; + int maxY = 0; + int offsetY = 0; + + float maxWidth = 0; + + // draw background first + { + Vector2D fullSize = {}; + int monitorsWithOverlayData = 0; + + for (const auto& m : g_pCompositor->m_monitors) { + const Vector2D size = m_monitorOverlays[m].size(); + if (size.x <= 0 || size.y <= 0) + continue; + + fullSize.y += size.y + OVERLAY_MONITOR_GAP; + fullSize.x = std::max(fullSize.x, size.x); + ++monitorsWithOverlayData; + } + + if (monitorsWithOverlayData > 0) { + fullSize.y -= OVERLAY_MONITOR_GAP; + + // Each monitor section is offset by OVERLAY_MARGIN_TOP + OVERLAY_BOX_MARGIN in CMonitorOverlay::draw, + // while the per-monitor drawn box height only tracks content (+2 px padding). + // Account for that inter-section offset so the backdrop spans stacked monitor overlays correctly. + fullSize.y += sc((monitorsWithOverlayData - 1) * (OVERLAY_MARGIN_TOP + OVERLAY_BOX_MARGIN - 2)); + } + + maxWidth = fullSize.x; + + if (fullSize.y > 1 && fullSize.x > 1) { + CRectPassElement::SRectData data; + data.box = CBox{{OVERLAY_MARGIN_LEFT, OVERLAY_MARGIN_TOP}, fullSize + Vector2D{OVERLAY_BOX_MARGIN, OVERLAY_BOX_MARGIN} * 2.F}; + data.color = CHyprColor{0.1F, 0.1F, 0.1F, 0.6F}; + data.round = 10; + data.blur = true; + g_pHyprRenderer->m_renderPass.add(makeUnique(std::move(data))); + + createWarningTexture(fullSize.x); + } + } + + for (auto const& monitor : g_pCompositor->m_monitors) { + bool monitorUpdated = false; + offsetY += m_monitorOverlays[monitor].draw(offsetY, monitorUpdated); + cacheUpdated = cacheUpdated || monitorUpdated; + + const auto& BOX = m_monitorOverlays[monitor].lastDrawnBox(); + if (BOX.width > 0 && BOX.height > 0) { + const int boxMinX = sc(std::floor(BOX.x)); + const int boxMinY = sc(std::floor(BOX.y)); + const int boxMaxX = sc(std::ceil(BOX.x + BOX.width)); + const int boxMaxY = sc(std::ceil(BOX.y + BOX.height)); + + if (!haveAnyBox) { + minX = boxMinX; + minY = boxMinY; + maxX = boxMaxX; + maxY = boxMaxY; + haveAnyBox = true; + } else { + minX = std::min(minX, boxMinX); + minY = std::min(minY, boxMinY); + maxX = std::max(maxX, boxMaxX); + maxY = std::max(maxY, boxMaxY); + } + } + + offsetY += OVERLAY_MONITOR_GAP; + } + + offsetY -= OVERLAY_MONITOR_GAP; + + // render warning texture + if (m_warningTexture) { + { + CRectPassElement::SRectData data; + data.box = CBox{{OVERLAY_MARGIN_LEFT, offsetY + (OVERLAY_MARGIN_TOP * 2) + OVERLAY_BOX_MARGIN}, + {maxWidth + (OVERLAY_BOX_MARGIN * 2.F), m_warningTexture->m_size.y + (OVERLAY_BOX_MARGIN * 2.F)}}; + data.color = CHyprColor{0.1F, 0.1F, 0.1F, 0.6F}; + data.round = 10; + data.blur = true; + g_pHyprRenderer->m_renderPass.add(makeUnique(std::move(data))); + } + + { + CTexPassElement::SRenderData data; + data.box = CBox{ + Vector2D{OVERLAY_MARGIN_LEFT + ((maxWidth - m_warningTexture->m_size.x) / 2.F), sc(offsetY) + (OVERLAY_MARGIN_TOP * 2) + (OVERLAY_BOX_MARGIN * 2)}.round(), + m_warningTexture->m_size}; + data.tex = m_warningTexture; + g_pHyprRenderer->m_renderPass.add(makeUnique(std::move(data))); + } + } + + CBox newDrawnBox; + if (haveAnyBox) + newDrawnBox = {sc(PMONITOR->m_position.x) + minX, sc(PMONITOR->m_position.y) + minY, maxX - minX, maxY - minY}; + + if (cacheUpdated || newDrawnBox != m_lastDrawnBox) { + if (m_lastDrawnBox.width > 0 && m_lastDrawnBox.height > 0) + g_pHyprRenderer->damageBox(m_lastDrawnBox); + + if (newDrawnBox.width > 0 && newDrawnBox.height > 0) + g_pHyprRenderer->damageBox(newDrawnBox); + + m_lastDrawnBox = newDrawnBox; + } + + if (m_frameTimer.getMillis() >= OVERLAY_REFRESH_INTERVAL_MS) { + g_pCompositor->scheduleFrameForMonitor(PMONITOR); + m_frameTimer.reset(); + } +} diff --git a/src/debug/Overlay.hpp b/src/debug/Overlay.hpp new file mode 100644 index 000000000..02f7f60f5 --- /dev/null +++ b/src/debug/Overlay.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include "../defines.hpp" +#include "../render/Texture.hpp" +#include "../helpers/time/Timer.hpp" +#include +#include +#include +#include + +namespace Render { + class IHyprRenderer; + class ITexture; +} + +namespace Debug { + + class CMonitorOverlay { + public: + int draw(int offset, bool& cacheUpdated); + const CBox& lastDrawnBox() const; + + void renderData(PHLMONITOR pMonitor, float durationUs); + void renderDataNoOverlay(PHLMONITOR pMonitor, float durationUs); + void frameData(PHLMONITOR pMonitor); + + Vector2D size() const; + + private: + struct STextLine { + std::string text; + CHyprColor color; + int fontSize = 10; + SP texture; + }; + + struct SMetricData { + float avg = 0.F; + float min = 0.F; + float max = 0.F; + float var = 0.F; + }; + + void updateLine(size_t idx, const std::string& text, const CHyprColor& color, int fontSize, const std::string& fontFamily); + void rebuildCache(); + + std::deque m_lastFrametimes; + std::deque m_lastFPSPerSecond; + std::deque m_lastRenderTimes; + std::deque m_lastRenderTimesNoOverlay; + std::deque m_lastAnimationTicks; + std::chrono::high_resolution_clock::time_point m_lastFrame; + std::chrono::high_resolution_clock::time_point m_fpsSecondStart; + size_t m_framesInCurrentSecond = 0; + PHLMONITORREF m_monitor; + + std::vector m_cachedLines; + bool m_cacheValid = false; + CTimer m_cacheTimer; + + CBox m_lastDrawnBox; + + friend class Render::IHyprRenderer; + }; + + class COverlay { + public: + COverlay(); + void draw(); + void renderData(PHLMONITOR, float durationUs); + void renderDataNoOverlay(PHLMONITOR, float durationUs); + void frameData(PHLMONITOR); + + private: + std::map m_monitorOverlays; + CBox m_lastDrawnBox; + CTimer m_frameTimer; + + void createWarningTexture(float maxW); + SP m_warningTexture; + float m_warningTextureMaxW = 0; + + friend class CHyprMonitorDebugOverlay; + friend class Render::IHyprRenderer; + }; + + UP& overlay(); +} diff --git a/src/render/GLRenderer.cpp b/src/render/GLRenderer.cpp index aefdc67a4..129295acd 100644 --- a/src/render/GLRenderer.cpp +++ b/src/render/GLRenderer.cpp @@ -9,7 +9,7 @@ #include "../protocols/PresentationTime.hpp" #include "../protocols/core/DataDevice.hpp" #include "../protocols/core/Compositor.hpp" -#include "../debug/HyprDebugOverlay.hpp" +#include "../debug/Overlay.hpp" #include "../helpers/Monitor.hpp" #include "pass/TexPassElement.hpp" #include "pass/SurfacePassElement.hpp" diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp index df1106b0f..acd83f8f2 100644 --- a/src/render/Renderer.cpp +++ b/src/render/Renderer.cpp @@ -25,7 +25,7 @@ #include "../protocols/LinuxDMABUF.hpp" #include "../helpers/sync/SyncTimeline.hpp" #include "../hyprerror/HyprError.hpp" -#include "../debug/HyprDebugOverlay.hpp" +#include "../debug/Overlay.hpp" #include "../notification/NotificationOverlay.hpp" #include "../layout/LayoutManager.hpp" #include "../layout/space/Space.hpp" @@ -1523,6 +1523,17 @@ SP IHyprRenderer::renderText(const std::string& text, CHyprColor col, return tex; } +SP IHyprRenderer::renderText(Hyprgraphics::CTextResource::STextResourceData&& data) { + auto res = makeAtomicShared(std::move(data)); + g_pAsyncResourceGatherer->enqueue(res); + g_pAsyncResourceGatherer->await(res); + + if (!res->m_asset.cairoSurface) + return nullptr; + + return createTexture(res->m_asset.pixelSize.x, res->m_asset.pixelSize.y, res->m_asset.cairoSurface->data()); +} + void IHyprRenderer::ensureLockTexturesRendered(bool load) { static bool loaded = false; @@ -1913,7 +1924,7 @@ void IHyprRenderer::renderMonitor(PHLMONITOR pMonitor, bool commit) { if (*PDEBUGOVERLAY == 1) { renderStart = std::chrono::high_resolution_clock::now(); - g_pDebugOverlay->frameData(pMonitor); + Debug::overlay()->frameData(pMonitor); } if (!g_pCompositor->m_sessionActive) @@ -2062,7 +2073,7 @@ void IHyprRenderer::renderMonitor(PHLMONITOR pMonitor, bool commit) { // for drawing the debug overlay if (pMonitor == g_pCompositor->m_monitors.front() && *PDEBUGOVERLAY == 1) { renderStartOverlay = std::chrono::high_resolution_clock::now(); - g_pDebugOverlay->draw(); + Debug::overlay()->draw(); endRenderOverlay = std::chrono::high_resolution_clock::now(); } @@ -2140,13 +2151,13 @@ void IHyprRenderer::renderMonitor(PHLMONITOR pMonitor, bool commit) { if (*PDEBUGOVERLAY == 1) { const float durationUs = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - renderStart).count() / 1000.f; - g_pDebugOverlay->renderData(pMonitor, durationUs); + Debug::overlay()->renderData(pMonitor, durationUs); if (pMonitor == g_pCompositor->m_monitors.front()) { const float noOverlayUs = durationUs - std::chrono::duration_cast(endRenderOverlay - renderStartOverlay).count() / 1000.f; - g_pDebugOverlay->renderDataNoOverlay(pMonitor, noOverlayUs); + Debug::overlay()->renderDataNoOverlay(pMonitor, noOverlayUs); } else - g_pDebugOverlay->renderDataNoOverlay(pMonitor, durationUs); + Debug::overlay()->renderDataNoOverlay(pMonitor, durationUs); } } @@ -2850,7 +2861,7 @@ bool IHyprRenderer::shouldRenderCursor() { } std::tuple IHyprRenderer::getRenderTimes(PHLMONITOR pMonitor) { - const auto POVERLAY = &g_pDebugOverlay->m_monitorOverlays[pMonitor]; + const auto POVERLAY = &Debug::overlay()->m_monitorOverlays[pMonitor]; float avgRenderTime = 0; float maxRenderTime = 0; diff --git a/src/render/Renderer.hpp b/src/render/Renderer.hpp index 42592f2ca..f52b9e3a0 100644 --- a/src/render/Renderer.hpp +++ b/src/render/Renderer.hpp @@ -31,6 +31,8 @@ #include "Framebuffer.hpp" #include "Texture.hpp" +#include + struct SMonitorRule; class CWorkspace; class CInputPopup; @@ -164,6 +166,7 @@ namespace Render { virtual SP createTexture(const SP buffer, bool keepDataCopy = false); virtual SP renderText(const std::string& text, CHyprColor col, int pt, bool italic = false, const std::string& fontFamily = "", int maxWidth = 0, int weight = 400); + virtual SP renderText(Hyprgraphics::CTextResource::STextResourceData&& data); SP loadAsset(const std::string& filename); virtual bool shouldUseNewBlurOptimizations(PHLLS pLayer, PHLWINDOW pWindow); virtual bool explicitSyncSupported() = 0;